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. 调用链分析
Runtime.getRuntime().exec(String command)最终调用exec(String[] cmdarray, String[] envp, File dir)- 使用
StringTokenizer将命令按空格分割成字符串数组 - 创建
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"echo123>/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 |
六、安全注意事项
- 永远不要直接执行未经验证的用户输入
- 使用白名单验证命令和参数
- 考虑使用更安全的 API 替代 Runtime.exec
- 注意命令注入防护
- 正确处理输入/输出流,避免阻塞
七、最佳实践
- 优先使用 ProcessBuilder 而非 Runtime.exec
- 明确指定命令和参数数组
- 正确处理命令执行的环境变量和工作目录
- 确保正确读取进程输出和错误流
- 考虑使用超时机制防止长时间阻塞
通过深入理解 Runtime.exec 的底层机制,开发者可以更安全有效地在 Java 应用程序中执行系统命令,同时避免常见的陷阱和安全问题。