Java代码审计–RCE漏洞

admin 2026-03-18 21:02:30 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档详细介绍了Java代码审计中RCE漏洞的原理与场景,指出根源在于用户输入未过滤导致的命令拼接。重点分析了Runtime.getRuntime().exec()和ProcessBuilder的使用方法与风险点,通过SpringBoot示例演示了命令注入过程及动态调试细节,解释了URL编码绕过参数解析的原理。最后提供了审计关键词,建议开发者严格校验输入参数,防止恶意命令执行。 综合评分: 88 文章分类: 代码审计,漏洞分析,WEB安全


cover_image

Java 代码审计 – RCE 漏洞

原创

GOWLSJ125 GOWLSJ125

走在网安路上的哥布林

2026年3月11日 16:52 福建

什么是 RCE 漏洞

概述

  RCE 是远程代码执行或远程命令执行,指当应用程序需要调用系统命令执行函数时,若开发人员未对用户可控的输入参数进行严格的校验、过滤或转义,攻击者可通过篡改这些参数,将恶意系统命令拼接至正常执行逻辑中,最终让服务器执行非预期的危险指令,从而实现命令注入攻击。

  这类漏洞的触发场景与修复逻辑可具体拆解:攻击者通常通过 WEB 界面、客户端接口等渠道提交构造好的恶意命令参数,而服务器端若存在两类问题 —— 一是未对执行系统命令的函数入参做任何安全过滤,二是业务逻辑设计存在缺陷(如参数拼接逻辑未做边界限制),就会导致恶意参数被直接带入命令执行流程。

  从本质来看,该漏洞的根源是开发人员在代码层面,未对可执行系统命令的敏感函数、自定义执行方法的入口参数做合规校验:既未过滤命令拼接的特殊字符(如 ;&&| 等命令分隔符),也未限制参数的合法范围,最终使得客户端提交的恶意指令能够绕过校验,被服务器端直接解析并执行。

总结三点

  1. RCE 漏洞的核心是用户可控参数未过滤,导致恶意命令被拼接至系统执行函数中;
  2. Java 场景下常见风险点为 Runtime.getRuntime().exec() 等系统命令执行函数或方法的参数处理不当;
  3. 漏洞本质是开发端未对敏感执行函数或方法的入参做校验、过滤或逻辑限制。

可能出现的场景

  1. 服务端直接存在可执行函数,如 Runtime.getRuntime().exec()ProcessBuilder 等,且对传入的参数过滤不严格导致 RCE 漏洞。
  2. 有表达式注入导致的 RCE 漏洞,常见的有 OGNLSpELMVELELFelJST+EL 等。
  3. 由 Java 后端模板引擎注入导致的 RCE 漏洞,如 FreemarkerVelocityThymeleaf 等。
  4. 由 Java 一些脚本语言引起从 RCE 漏洞,如 GroovyJavascriptEngine 等。

可执行函数导致的 RCE 漏洞

Runtime.getRuntime().exec() 导致的 RCE

Runtime.getRuntime().exec() 概述

java.lang.Runtime 公共类中的 exec() 用于在运行时执行外部操作系统命令。它接受用户提供的命令字符串,并将其传递给操作系统的命令解析器,从而允许用户执行系统级操作。

基本用法

共有以下 6 种使用方式:

// 在单独的进程中执行指定的字符串命令
@Deprecated(since="18")
public Process exec(String command) throws IOException {
    return exec(command, null, null);
}

// 在具有指定环境的单独进程中执行指定的字符串命令
@Deprecated(since="18")
public Process exec(String command, String[] envp) throws IOException {
    return exec(command, envp, null);
}

// 在具有指定环境和工作目录的单独进程中执行指定的字符串命令
@Deprecated(since="18")
public Process exec(String command, String[] envp, File dir)
    throws IOException {
    if (command.isEmpty())
        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();
}
// 1. exec(String command) - 已弃用
// 执行单个字符串命令,无法处理带空格的参数
Process p1 = Runtime.getRuntime().exec("notepad.exe");

// 2. exec(String command, String[] envp) - 已弃用
// 带环境变量的形式
tring[] env2 = {"PATH=/usr/bin", "JAVA_HOME=/opt/java"};
Process p2 = Runtime.getRuntime().exec("echo Hello", env2);

// 3. exec(String command, String[] envp, File dir) - 已弃用
// 带环境变量和工作目录
String[] env3 = {"MY_VAR=test"};
Process p3 = Runtime.getRuntime().exec("cmd /c dir", env3, new File("C:\\"));

// 4. exec(String[] cmdarray)
// 数组形式
Process p4 = Runtime.getRuntime().exec(new String[]{"notepad.exe", "test.txt"});

// 5. exec(String[] cmdarray, String[] envp)
// 带环境变量的数组形式
String[] cmd5 = {"java", "-version"};
String[] env5 = {"JAVA_HOME=D:\\Program Files\\Java\\jdk-21"};
Process p5 = Runtime.getRuntime().exec(cmd5, env5);

// 6. exec(String[] cmdarray, String[] envp, File dir)
// 命令数组 + 环境变量 + 工作目录
String[] cmd6 = {"cmd", "/c", "dir"};
String[] env6 = {"MY_PROJECT=hello"};
Process p6 = Runtime.getRuntime().exec(cmd6, env6, new File("D:\\workspace"));

示例

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
&nbsp; &nbsp; <modelVersion>4.0.0</modelVersion>
&nbsp; &nbsp; <parent>
&nbsp; &nbsp; &nbsp; &nbsp; <groupId>org.springframework.boot</groupId>
&nbsp; &nbsp; &nbsp; &nbsp; <artifactId>spring-boot-starter-parent</artifactId>
&nbsp; &nbsp; &nbsp; &nbsp; <version>4.0.3</version>
&nbsp; &nbsp; &nbsp; &nbsp; <relativePath/> <!-- lookup parent from repository -->
&nbsp; &nbsp; </parent>
&nbsp; &nbsp; <groupId>demo.rce</groupId>
&nbsp; &nbsp; <artifactId>RceDemo</artifactId>
&nbsp; &nbsp; <version>0.0.1-SNAPSHOT</version>
&nbsp; &nbsp; <name>RceDemo</name>
&nbsp; &nbsp; <description>RceDemo</description>
&nbsp; &nbsp; <url/>
&nbsp; &nbsp; <licenses>
&nbsp; &nbsp; &nbsp; &nbsp; <license/>
&nbsp; &nbsp; </licenses>
&nbsp; &nbsp; <developers>
&nbsp; &nbsp; &nbsp; &nbsp; <developer/>
&nbsp; &nbsp; </developers>
&nbsp; &nbsp; <scm>
&nbsp; &nbsp; &nbsp; &nbsp; <connection/>
&nbsp; &nbsp; &nbsp; &nbsp; <developerConnection/>
&nbsp; &nbsp; &nbsp; &nbsp; <tag/>
&nbsp; &nbsp; &nbsp; &nbsp; <url/>
&nbsp; &nbsp; </scm>
&nbsp; &nbsp; <properties>
&nbsp; &nbsp; &nbsp; &nbsp; <java.version>21</java.version>
&nbsp; &nbsp; </properties>
&nbsp; &nbsp; <dependencies>
&nbsp; &nbsp; &nbsp; &nbsp; <dependency>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <groupId>org.springframework.boot</groupId>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <artifactId>spring-boot-starter-webmvc</artifactId>
&nbsp; &nbsp; &nbsp; &nbsp; </dependency>
&nbsp; &nbsp; </dependencies>

&nbsp; &nbsp; <build>
&nbsp; &nbsp; &nbsp; &nbsp; <plugins>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <plugin>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <groupId>org.springframework.boot</groupId>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <artifactId>spring-boot-maven-plugin</artifactId>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </plugin>
&nbsp; &nbsp; &nbsp; &nbsp; </plugins>
&nbsp; &nbsp; </build>

</project>

Controller

@RestController
public class RceController {
&nbsp; &nbsp; @GetMapping("/exec")
&nbsp; &nbsp; public String executeCommand(@RequestParam("ip") String ip) {
&nbsp; &nbsp; &nbsp; &nbsp; StringBuilder output = new StringBuilder();
&nbsp; &nbsp; &nbsp; &nbsp; // 直接拼接用户输入 - 危险
&nbsp; &nbsp; &nbsp; &nbsp; String[] command = {"cmd", "/c", "ping " + ip};
&nbsp; &nbsp; &nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 执行系统命令
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Process process = Runtime.getRuntime().exec(command);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 读取命令输出
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; BufferedReader reader = new BufferedReader(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new InputStreamReader(process.getInputStream(),"GBK")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; );

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; String line;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while ((line = reader.readLine()) != null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output.append(line).append("\n");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 等待命令执行完成
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; int exitCode = process.waitFor();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (output.isEmpty()) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output.append("命令执行完成,退出码: ").append(exitCode);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; reader.close();

&nbsp; &nbsp; &nbsp; &nbsp; } catch (Exception e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output.append("执行出错: ").append(e.getMessage());
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; return output.toString();
&nbsp; &nbsp; }
}

Payload:127.0.0.1%20%26%20calc%20=空格,%26=&

Windows 命令连接符: & : 执行多个命令(无论前面是否成功) && : 前面的命令成功才执行后面的 | : 管道,将前面的输出作为后面的输入 || : 前面的命令失败才执行后面的

动态调试过程

进入到 exec 方法中。

调用了 ProcessBuilder 执行。

为什么要编码

  这是一个关于 HTTP 协议和 URL 参数传输的技术问题。

 ?ip=127.0.0.1 & whoami

  ↓ 服务器解析

 参数1: ip = “127.0.0.1 “

 参数2: whoami = “” (空值)

  所以实际接收到为:http://127.0.0.1:8080/exec?ip=127.0.0.1 & whoami=,ip 参数只是 127.0.0.1 ,后面的被截断了。

  URL 编码后接收到的:http://localhost:8080/exec?ip=127.0.0.1%20%26%20whoami

服务器收到: ip = “127.0.0.1 & whoami”

执行命令: ping 127.0.0.1 & whoami

ProcessBuilder 导致的 RCE

ProcessBuilder 类概述

ProcessBuilder 是 Java 提供的一个用于创建操作系统进程的类,它位于 java.lang 包中。它的主要作用是启动和管理外部进程。

主要作用

  1. 创建和启动外部进程
  • ProcessBuilder 允许在 Java 程序中启动外部应用程序或系统命令,比如:

  • 执行系统命令(如 dirlsping 等)

  • 启动其他 Java 程序

  • 运行脚本文件

  • 调用任何可执行文件

  1. 进程配置和控制
  • 设置工作目录
  • 配置环境变量
  • 重定向输入/输出流
  • 合并错误流和标准输出流

示例

p@GetMapping("/exec2")
public String executeCommand2(@RequestParam("ip") String ip) {
&nbsp; &nbsp; StringBuilder output = new StringBuilder();
&nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; // 使用 ProcessBuilder
&nbsp; &nbsp; &nbsp; &nbsp; ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "ping " + ip);

&nbsp; &nbsp; &nbsp; &nbsp; // 启动进程
&nbsp; &nbsp; &nbsp; &nbsp; Process process = pb.start();

&nbsp; &nbsp; &nbsp; &nbsp; // 读取命令输出
&nbsp; &nbsp; &nbsp; &nbsp; BufferedReader reader = new BufferedReader(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new InputStreamReader(process.getInputStream(), "GBK")
&nbsp; &nbsp; &nbsp; &nbsp; );

&nbsp; &nbsp; &nbsp; &nbsp; String line;
&nbsp; &nbsp; &nbsp; &nbsp; while ((line = reader.readLine()) != null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output.append(line).append("\n");
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; int exitCode = process.waitFor();

&nbsp; &nbsp; &nbsp; &nbsp; if (output.isEmpty()) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output.append("命令执行完成,退出码: ").append(exitCode);
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; reader.close();

&nbsp; &nbsp; } catch (Exception e) {
&nbsp; &nbsp; &nbsp; &nbsp; output.append("执行出错: ").append(e.getMessage());
&nbsp; &nbsp; }

&nbsp; &nbsp; return output.toString();
}

Payload:127.0.0.1%20%26%20dir%20=空格,%26=&


审计关键词

在代码审计中,搜索以下关键词:

  • Runtime.getRuntime().exec(
  • ProcessBuilder(
  • Process
  • .waitFor()


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:走在网安路上的哥布林 GOWLSJ125 GOWLSJ125《Java 代码审计 – RCE 漏洞》

    评论:0   参与:  0