【代码审计】命令执行漏洞分析

【代码审计】命令执行漏洞分析

Sec探索者 2025-05-19 00:52

点击上方蓝字关注我们

请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,作者不为此承担任何责任。如有侵权烦请告知,我们会立即删除并致歉。谢谢!

01

漏洞简介

在Java代码审计中,命令执行漏洞指应用程序未对用户输入进行严格过滤,直接将外部可控参数拼接到系统命令中执行,导致攻击者可注入恶意命令并获取服务器控制权。其核心原理是开发者误用危险函数执行系统命令时,未对用户输入的参数进行安全校验或转义,导致用户可通过构造特殊字符(如管道符
|
、命令分隔符;
)或参数注入(如${}
表达式)将恶意指令与原始命令拼接。该漏洞常出现在参数动态拼接的场景,且受操作系统特性影响。

Windows系统命令注入表

Linux系统命令注入表

02

Java原生命令执行方式

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
去执行,命令被错误的分割成
[“calc&ping”, “baidu.com”]
 ,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
即可

ProcessBuilder

ProcessBuilder命令
执行漏洞的核心在于通过
ProcessBuilder
类直接构造并执行系统命令时,
若未对用户输入参数进行严格过滤或拆分
,攻击者可注入恶意命令实现任意代码执行,
Pr
ocessBuilder
不支持以字符串形式传入命令,只能拆分成List或者数组的形式传入。

从靶场漏洞代码中可知道,是直接将用户输入的参数传入到
ProcessBuilder
进行命令执行,从而攻击者可以拼接恶意命令造成漏洞

跟进
ProcessBuilder.start()
方法,
ProcessBuilder.start()
 是Java中启动外部进程的核心方法,其底层实现最终通过调用 
ProcessImpl.start() 
完成操作系统级别的进程创建。

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

靶场效果

03

代码审计思路

学习完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)
     传递单个字符串命令

  • 检查反射绕过,
    通过反射直接调用ProcessImpl.start()
    ,绕过参数安全检查。

04

防御与修复

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

陪伴是最长情的告白

为你推送最实用的网安知识

识别二维码

关注我们