0x01 漏洞简介
在Java代码审计中,命令执行漏洞指应用程序未对用户输入进行严格过滤,直接将外部可控参数拼接到系统命令中执行,导致攻击者可注入恶意命令并获取服务器控制权。其核心原理是开发者误用危险函数执行系统命令时,未对用户输入的参数进行安全校验或转义,导致用户可通过构造特殊字符(如管道符|
、命令分隔符;
)或参数注入(如${}
表达式)将恶意指令与原始命令拼接。该漏洞常出现在参数动态拼接的场景,且受操作系统特性影响。
Windows系统命令注入表
Linux系统命令注入表
0x02 Java命令执行方法
2.1 Runtime.exec()
Runtime.exec()
是Java中执行系统命令的核心方法,提供多种重载形式,本质是启动子进程执行外部命令。直接拼接用户输入会导致命令注入漏洞,需使用参数数组形式并严格校验输入。Java中命令执行用到最多的方法就是java.lang.Runtime#exec()
。
// 方法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()方法在执行命令的时候,传入的参数有字符串和字符串数组两种形参,将这种方法封装到靶场上且限制必须包含ping来执行观察其效果,分析可利用的方式。
首先使用exec(String)执行ping命令,正常返回结果,执行其他命令则异常
使用&
进行命令拼接,但是发现也异常了
切换为exec(String[])
时,直接使用&
拼接,发现两条命令都可以被执行。
思考下,为什么exec(String)
使用了&
会异常,而exec(String[])
不会呢?来动态调试exec(String)
的调用链加深理解,开始断点调试,发现调用的是exec(command, null, null);
继续跟进,发现这里最终也去调用的是exec(cmdarray, envp, dir)
,那为什么最终的命令执行的结果不一样呢,分析下面的代码可以发现,传入的command
是经过了StringTokenizer
进行处理的,那么问题的关键就是StringTokenizer
是怎么去处理的
跟进StringTokenizer
,发现这里对传入的命令做了一个分割,将传入的字符串str
按照默认的空白分隔符(包括空格、制表符t
、换行符n
、回车符r
和换页符f
)进行分割,将字符串拆解为一系列连续的子字符串
String[] cmdarray = new String[st.countTokens()];
的作用是创建一个字符串数组cmdarray
,其长度等于StringTokenizer
对象st
中分割后的子字符串数量,最终命令变成了["calc&ping","baidu.com"]
传入了exec(cmdarray, envp, dir)
最终交给ProcessBuilder
去执行,命令被错误的分割成<font style="color:rgba(0, 0, 0, 0.88);">["calc&ping", "baidu.com"]</font>
,Java会尝试执行第一个参数calc&ping
,视为可执行程序名,calc&ping
不是一个有效程序,因此抛出IOException: Cannot run program "calc&ping"
。
如果是exec(String[])
呢?最终命令是["cmd", "/c", "calc&ping", "baidu.com"]
,Java调用cmd.exe
,并传递参数/c
和后续参数calc&ping baidu.com
。cmd.exe
会将calc&ping baidu.com
整体视为一个字符串,并按照 Shell 规则解析其中的&
符号,将calc&ping baidu.com
解析为两条命令,故而执行成功。
那exec(String)
怎么去进行漏洞利用呢?可以利用Shell的解析逻辑实现命令注入,直接拼接cmd /c
即可
2.2 ProcessBuilder
ProcessBuilder命令执行漏洞的核心在于通过ProcessBuilder
类直接构造并执行系统命令时,若未对用户输入参数进行严格过滤或拆分,攻击者可注入恶意命令实现任意代码执行,ProcessBuilder
不支持以字符串形式传入命令,只能拆分成List或者数组的形式传入。
从靶场漏洞代码中可知道,是直接将用户输入的参数传入到ProcessBuilder
进行命令执行,从而攻击者可以拼接恶意命令造成漏洞
跟进ProcessBuilder.start()
方法,ProcessBuilder.start()
是Java中启动外部进程的核心方法,其底层实现最终通过调用ProcessImpl.start()
完成操作系统级别的进程创建。
2.3 ProcessImpl
ProcessImpl
是 Java 中Process
抽象类的具体实现类 ,其设计目的是为ProcessBuilder.start()
方法提供底层支持,用于创建和管理操作系统进程 。由于ProcessImpl
的构造函数被声明为private
,无法直接通过new
实例化,开发者通常需通过ProcessBuilder
或Runtime.exec()
间接调用其功能 。若需直接操作ProcessImpl
,必须通过反射技术绕过访问限制。ProcessImpl
的start()
方法是静态的,可通过反射获取该方法并传入参数(如命令数组、环境变量等)创建进程。
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 代码审计思路
学习完Java命令执行的一些方式,就可以知道需要审计哪些危险函数了,是否手动创建了shell,然后关注参数是否可控,若不可控则无法命令注入,若参数可控注意空格等会导致分割,可编码绕过(如${IFS}
代替空格)。核心思路是识别所有可能导致命令注入的代码路径,重点围绕“参数可控性”和“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解释器,如new ProcessBuilder("sh", "-c", userCmd)
-
检查参数分割逻辑,如使用
exec(String command)
传递单个字符串命令 -
检查反射绕过,通过反射直接调用
<font style="color:rgba(0, 0, 0, 0.88);background-color:rgb(246, 246, 246);">ProcessImpl.start()</font>
,绕过参数安全检查。
0x04 防御与修复
1、避免执行系统命令
优先使用Java原生API替代直接执行系统命令。例如:删除文件使用File.delete()
而非rm
命令 ,网络请求使用通过HttpClient
而非curl
命令等等,规避命令注入风险,同时提升跨平台兼容性。
2、无法避免系统命令执行时,优先使用Runtime.exec(String[] cmdarray)
或ProcessBuilder
的数组传参方式,避免将命令与参数拼接为字符串
exec("cmd /c " + userInput) // 容易被注入
exec(new String[]{"cmd", "/c", fixedCommand}) // 如果命令有白名单限制,相比上面安全
3、避免shell调用,禁止通过sh -c
、cmd.exe /c
等方式创建Shell环境,直接调用可执行文件路径。
exec("sh -c ls " + dir); // 不安全的Shell调用
exec(new String[]{"/bin/ls", dir}); // 安全
4、危险字符过滤,过滤|
、&
、;
、$()
等Shell元字符,以及路径遍历符号(../
),可使用OWASP ESAPI等安全库进行编码处理
String safeInput = ESAPI.encoder().encodeForOS(new WindowsCodec(), userInput);
原文始发于微信公众号(船山信安):Java代码审计之命令执行漏洞详解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论