Java代码审计之命令执行漏洞详解
0x01 漏洞简介
在Java代码审计中,命令执行漏洞指应用程序未对用户输入进行严格过滤,直接将外部可控参数拼接到系统命令中执行,导致攻击者可注入恶意命令并获取服务器控制权。
核心原理:
- 开发者误用危险函数执行系统命令时,未对用户输入的参数进行安全校验或转义
- 用户可通过构造特殊字符(如管道符
|、命令分隔符;)或参数注入(如${}表达式)将恶意指令与原始命令拼接 - 该漏洞常出现在参数动态拼接的场景,且受操作系统特性影响
操作系统特性影响
Windows系统命令注入表:
&- 命令分隔符&&- 前一个命令成功时执行后一个命令|- 管道符||- 前一个命令失败时执行后一个命令%0a- 换行符%0d- 回车符^- 转义字符
Linux系统命令注入表:
;- 命令分隔符&&- 前一个命令成功时执行后一个命令|- 管道符||- 前一个命令失败时执行后一个命令$()- 命令替换` `- 反引号命令替换\n- 换行符\r- 回车符
0x02 Java命令执行方法
2.1 Runtime.exec()
Runtime.exec() 是Java中执行系统命令的核心方法,提供多种重载形式,本质是启动子进程执行外部命令。直接拼接用户输入会导致命令注入漏洞,需使用参数数组形式并严格校验输入。
方法重载形式:
// 方法1: 直接执行字符串命令
Process exec(String command);
// 方法2: 通过字符串数组传递命令和参数
Process exec(String[] cmdarray);
// 方法3: 指定环境变量执行字符串命令
Process exec(String command, String[] envp);
// 方法4: 通过数组传递命令并自定义环境变量
Process exec(String[] cmdarray, String[] envp);
// 方法5: 指定环境变量和工作目录执行字符串命令
Process exec(String command, String[] envp, File dir);
// 方法6: 通过数组传递命令,并指定环境变量、工作目录
Process exec(String[] cmdarray, String[] envp, File dir);
执行差异分析:
-
exec(String)与exec(String[])的区别:exec(String)会将传入的命令字符串通过StringTokenizer进行分割,按照默认的空白分隔符(包括空格、制表符\t、换行符\n、回车符\r和换页符\f)进行分割exec(String[])则直接使用传入的数组作为命令参数
-
漏洞利用差异:
exec(String)中使用&拼接命令会异常,因为命令被错误分割exec(String[])中使用&拼接命令可以成功执行,因为整个字符串会被Shell解析
-
exec(String)的漏洞利用方法:- 可以利用Shell的解析逻辑实现命令注入,直接拼接
cmd /c即可
- 可以利用Shell的解析逻辑实现命令注入,直接拼接
2.2 ProcessBuilder
ProcessBuilder 命令执行漏洞的核心在于通过 ProcessBuilder 类直接构造并执行系统命令时,若未对用户输入参数进行严格过滤或拆分,攻击者可注入恶意命令实现任意代码执行。
特点:
ProcessBuilder不支持以字符串形式传入命令,只能拆分成List或者数组的形式传入- 底层实现最终通过调用
ProcessImpl.start()完成操作系统级别的进程创建
2.3 ProcessImpl
ProcessImpl 是Java中 Process 抽象类的具体实现类,其设计目的是为 ProcessBuilder.start() 方法提供底层支持,用于创建和管理操作系统进程。
特点:
ProcessImpl的构造函数被声明为private,无法直接通过new实例化- 开发者通常需通过
ProcessBuilder或Runtime.exec()间接调用其功能 - 若需直接操作
ProcessImpl,必须通过反射技术绕过访问限制
反射调用示例:
package com.xmsec.controller.rce;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
public class ProcessImplExamples {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
String[] cmdarray = new String[]{"cmd", "/c", "calc"};
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process p = (Process) method.invoke(null, cmdarray, null, null, null, false);
}
}
0x03 代码审计思路
核心思路是识别所有可能导致命令注入的代码路径,重点围绕"参数可控性"和"Shell调用方式"两个维度进行分析:
1. 定位危险入口点
识别所有可能执行系统命令的代码位置:
Runtime.getRuntime().exec()ProcessBuilder.start()- 反射调用
ProcessImpl、UNIXProcess等底层类的方法
审计技巧:
- 使用IDE全局搜索关键词:
exec(、ProcessBuilder、start(、getRuntime() - 检查反射调用:搜索
Class.forName()、Method.invoke()等代码块,确认是否操作危险类(如ProcessImpl)
2. 分析参数来源
判断命令参数是否完全或部分可控:
- HTTP请求参数(GET/POST)、Headers、Cookies
- 文件上传内容、数据库查询结果
- 配置文件(如YAML/Properties)中的动态值
- 是否存在字符串拼接(如
"sh -c " + userInput) - 是否通过
String.format()、StringBuilder动态生成命令
3. 验证调用方式与参数解析
确认是否通过Shell环境执行命令,以及参数解析是否安全:
- Shell会解析命令中的特殊符号(如
;、&&、$()),导致命令注入 - 检查是否显式调用Shell,如使用
sh -c、bash -c、cmd.exe /c等Shell解释器 - 检查参数分割逻辑,如使用
exec(String command)传递单个字符串命令 - 检查反射绕过,通过反射直接调用
ProcessImpl.start(),绕过参数安全检查
0x04 防御与修复
1. 避免执行系统命令
优先使用Java原生API替代直接执行系统命令。例如:
- 删除文件使用
File.delete()而非rm命令 - 网络请求使用
HttpClient而非curl命令
这样可以规避命令注入风险,同时提升跨平台兼容性。
2. 使用安全的命令执行方式
无法避免系统命令执行时,优先使用:
// 不安全的字符串拼接方式
exec("cmd /c " + userInput);
// 安全的数组传参方式
exec(new String[]{"cmd", "/c", fixedCommand});
3. 避免Shell调用
禁止通过 sh -c、cmd.exe /c 等方式创建Shell环境,直接调用可执行文件路径:
// 不安全的Shell调用
exec("sh -c ls " + dir);
// 安全的直接调用
exec(new String[]{"/bin/ls", dir});
4. 危险字符过滤
过滤 |、&、;、$() 等Shell元字符,以及路径遍历符号(../),可使用OWASP ESAPI等安全库进行编码处理:
String safeInput = ESAPI.encoder().encodeForOS(new WindowsCodec(), userInput);
5. 其他防御措施
- 实施最小权限原则,使用低权限账户执行命令
- 对用户输入实施白名单验证
- 对命令参数进行严格的类型和格式检查
- 记录和监控命令执行操作
总结
Java命令执行漏洞是Web应用安全中的高风险漏洞,审计时需要重点关注命令执行函数的调用方式、参数来源和解析逻辑。通过使用安全的API调用方式、避免Shell调用、严格过滤用户输入等措施,可以有效防御此类漏洞。在代码审计过程中,应当全面检查所有可能的命令执行路径,确保没有可控参数能够注入恶意命令。