JAVA 安全 | 深入分析 Runtime.exec 命令执行底层机制
字数 1712 2025-08-20 18:18:11

Java Runtime.exec 命令执行底层机制深入分析

一、Runtime.exec 方法概述

Runtime.getRuntime().exec() 是 Java 中执行系统命令的经典方法。该方法在 Windows 和 Linux 系统下的底层实现机制有所不同,本文将深入分析其调用链和底层实现。

Runtime.exec 方法重载

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

public Process exec(String command, String[] envp) throws IOException {
    return exec(command, envp, null);
}

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

public Process exec(String cmdarray[]) throws IOException {
    return exec(cmdarray, null, null);
}

public Process exec(String[] cmdarray, String[] envp) throws IOException {
    return exec(cmdarray, envp, null);
}

public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

二、Windows 系统下的执行机制

1. 基本执行流程

// 示例代码
InputStream is = Runtime.getRuntime().exec("whoami").getInputStream();
ByteArrayOutputStream resData = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > 0) {
    resData.write(buffer, 0, len);
}
System.out.println("命令执行结果: " + new String(resData.toByteArray()));

2. 调用链分析

  1. Runtime.getRuntime().exec(String command) 最终调用 exec(String[] cmdarray, String[] envp, File dir)
  2. 使用 StringTokenizer 将命令按空格分割成字符串数组
  3. 创建 ProcessBuilder 实例并调用 start() 方法

3. ProcessBuilder 直接实例化

Process process = new ProcessBuilder(new String[]{"whoami"}).start();
InputStream inputStream = process.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
    System.out.println(new String(buffer, 0, len));
}

4. ProcessImpl.start 反射调用

Class<?> clazz = Class.forName("java.lang.ProcessImpl");
Method start = clazz.getDeclaredMethod("start", String[].class, Map.class, 
    String.class, ProcessBuilder.Redirect[].class, boolean.class);
start.setAccessible(true);
Process process = (Process) start.invoke(null, 
    new String[]{"whoami"}, null, null, null, false);
// 读取输入流...

5. ProcessImpl.create 反射调用

Class<?> clazz = Class.forName("java.lang.ProcessImpl");
Method createMethod = clazz.getDeclaredMethod("create", 
    String.class, String.class, String.class, long[].class, boolean.class);
createMethod.setAccessible(true);
long pid = (long) createMethod.invoke(null, 
    "cmd /c calc", null, null, new long[]{-1, -1, -1}, false);

6. Windows 底层实现

最终调用 Windows API CreateProcessW:

BOOL CreateProcessW(
  LPCWSTR               lpApplicationName,
  LPWSTR                lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCWSTR               lpCurrentDirectory,
  LPSTARTUPINFOW        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

关键点:

  • 只能启动一个 SHELL
  • 执行批处理命令必须通过 cmd.exe /c
  • 处理路径中的空格时会自动添加双引号

三、Linux 系统下的执行机制

1. 问题示例

// 错误示例
Process process = Runtime.getRuntime().exec("/bin/sh -c \"echo 123 > /tmp/1.txt\"");
// 会报错: 123: 1: Syntax error: Unterminated quoted string

2. 正确执行方式

// 正确方式
Process process = Runtime.getRuntime().exec(
    new String[]{"/bin/sh", "-c", "echo 123 > /tmp/1.txt"});

3. StringTokenizer 的影响

StringTokenizer 会将命令按空格分割,导致:

  • /bin/sh -c "echo 123 > /tmp/1.txt" 被分割为:
    • /bin/sh
    • -c
    • "echo
    • 123
    • >
    • /tmp/1.txt"

4. UNIXProcess 反射调用

Class<?> clazz = Class.forName("java.lang.UNIXProcess");
Constructor<?> constructor = clazz.getDeclaredConstructor(
    byte[].class, byte[].class, int.class, 
    byte[].class, int.class, byte[].class, 
    int[].class, boolean.class);
constructor.setAccessible(true);

Object result = constructor.newInstance(
    "/bin/sh ".replace(" ", "\0").getBytes(), 
    "-c whoami ".replace(" ", "\0").getBytes(), 
    2, null, 0, null, 
    new int[]{-1, -1, -1}, false);

Method inputStreamMethod = clazz.getDeclaredMethod("getInputStream");
inputStreamMethod.setAccessible(true);
InputStream inputStream = (InputStream) inputStreamMethod.invoke(result);
// 读取输入流...

5. Linux 底层实现

最终调用 forkAndExec 原生方法:

JNIEXPORT jint JNICALL
Java_java_lang_UNIXProcess_forkAndExec(
    JNIEnv *env, jobject process, 
    jint mode, jbyteArray helperpath, 
    jbyteArray prog, jbyteArray argBlock, 
    jint argc, jbyteArray envBlock, 
    jint envc, jbyteArray dir, 
    jintArray std_fds, 
    jboolean redirectErrorStream)

四、跨平台问题解决方案

1. 通用解决方案

使用字符串数组而非单个字符串:

// 推荐方式
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "echo 123 > file"});
// 而不是
Runtime.getRuntime().exec("/bin/sh -c \"echo 123 > file\"");

2. 特殊字符处理技巧

// 使用 ${IFS} 代替空格
Runtime.getRuntime().exec("bash -c echo${IFS}heihu577");

3. Base64 编码方案

# Bash
bash -c {echo,<base64>}|{base64,-d}|{bash,-i}

# PowerShell
powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc <base64>

# Python
python -c exec('<base64>'.decode('base64'))

# Perl
perl -MMIME::Base64 -e eval(decode_base64('<base64>'))

五、关键差异总结

特性 Windows Linux
最终调用 ProcessImpl.create UNIXProcess 构造函数
命令处理 重新拼接为字符串 保持为参数数组
空格处理 自动添加双引号 需要特别注意参数分割
批处理命令 必须通过 cmd.exe /c 可直接通过 sh -c
原生调用 CreateProcessW forkAndExec

六、安全注意事项

  1. 永远不要直接执行未经验证的用户输入
  2. 使用白名单验证命令和参数
  3. 考虑使用更安全的 API 替代 Runtime.exec
  4. 注意命令注入防护
  5. 正确处理输入/输出流,避免阻塞

七、最佳实践

  1. 优先使用 ProcessBuilder 而非 Runtime.exec
  2. 明确指定命令和参数数组
  3. 正确处理命令执行的环境变量和工作目录
  4. 确保正确读取进程输出和错误流
  5. 考虑使用超时机制防止长时间阻塞

通过深入理解 Runtime.exec 的底层机制,开发者可以更安全有效地在 Java 应用程序中执行系统命令,同时避免常见的陷阱和安全问题。

Java Runtime.exec 命令执行底层机制深入分析 一、Runtime.exec 方法概述 Runtime.getRuntime().exec() 是 Java 中执行系统命令的经典方法。该方法在 Windows 和 Linux 系统下的底层实现机制有所不同,本文将深入分析其调用链和底层实现。 Runtime.exec 方法重载 二、Windows 系统下的执行机制 1. 基本执行流程 2. 调用链分析 Runtime.getRuntime().exec(String command) 最终调用 exec(String[] cmdarray, String[] envp, File dir) 使用 StringTokenizer 将命令按空格分割成字符串数组 创建 ProcessBuilder 实例并调用 start() 方法 3. ProcessBuilder 直接实例化 4. ProcessImpl.start 反射调用 5. ProcessImpl.create 反射调用 6. Windows 底层实现 最终调用 Windows API CreateProcessW : 关键点: 只能启动一个 SHELL 执行批处理命令必须通过 cmd.exe /c 处理路径中的空格时会自动添加双引号 三、Linux 系统下的执行机制 1. 问题示例 2. 正确执行方式 3. StringTokenizer 的影响 StringTokenizer 会将命令按空格分割,导致: /bin/sh -c "echo 123 > /tmp/1.txt" 被分割为: /bin/sh -c "echo 123 > /tmp/1.txt" 4. UNIXProcess 反射调用 5. Linux 底层实现 最终调用 forkAndExec 原生方法: 四、跨平台问题解决方案 1. 通用解决方案 使用字符串数组而非单个字符串: 2. 特殊字符处理技巧 3. Base64 编码方案 五、关键差异总结 | 特性 | Windows | Linux | |---------------------|----------------------------------|--------------------------------| | 最终调用 | ProcessImpl.create | UNIXProcess 构造函数 | | 命令处理 | 重新拼接为字符串 | 保持为参数数组 | | 空格处理 | 自动添加双引号 | 需要特别注意参数分割 | | 批处理命令 | 必须通过 cmd.exe /c | 可直接通过 sh -c | | 原生调用 | CreateProcessW | forkAndExec | 六、安全注意事项 永远不要直接执行未经验证的用户输入 使用白名单验证命令和参数 考虑使用更安全的 API 替代 Runtime.exec 注意命令注入防护 正确处理输入/输出流,避免阻塞 七、最佳实践 优先使用 ProcessBuilder 而非 Runtime.exec 明确指定命令和参数数组 正确处理命令执行的环境变量和工作目录 确保正确读取进程输出和错误流 考虑使用超时机制防止长时间阻塞 通过深入理解 Runtime.exec 的底层机制,开发者可以更安全有效地在 Java 应用程序中执行系统命令,同时避免常见的陷阱和安全问题。