1. 初识SSTI:当模板引擎“叛变”时
大家好,我是老张,在安全圈摸爬滚打十几年了。今天咱们来聊聊一个在Web安全中既经典又危险的漏洞——SSTI,也就是服务器端模板注入。我第一次遇到这个漏洞是在2015年,当时一个客户的电商网站被黑了,攻击者通过商品评论框注入了模板代码,直接拿到了服务器权限。从那以后,我就对这个漏洞特别上心。
简单来说,SSTI就像是模板引擎的“叛变”。模板引擎本来是个好帮手,它让前后端分离,程序员写逻辑,设计师写界面,各司其职。比如你用Flask开发网站,可能会写这样的代码:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route('/hello')
def hello():
name = request.args.get('name', 'Guest')
return render_template('hello.html', name=name)
这里的hello.html就是模板文件,里面可能有<h1>Hello {
{ name }}!</h1>。当用户访问/hello?name=张三时,模板引擎会把{
{ name }}替换成“张三”,然后返回给浏览器。一切都那么美好,直到有一天,程序员偷了个懒:
@app.route('/vulnerable')
def vulnerable():
name = request.args.get('name', 'Guest')
template = f"<h1>Hello {name}!</h1>"
return render_template_string(template)
看到问题了吗?这里直接把用户输入的name拼接到模板字符串里,然后用render_template_string渲染。如果用户输入{
{ 7*7 }},模板引擎会老老实实地计算7*7,返回“Hello 49!”。这就是SSTI的起点——用户输入被当成了模板代码来执行。
我经常跟团队里的新人说,模板引擎就像个“翻译官”,它把模板语言翻译成HTML。正常情况下,翻译官只翻译我们给它的“剧本”(模板文件)。但SSTI发生时,用户居然能修改剧本,还能往里加自己的“台词”,翻译官还傻乎乎地照单全收。更可怕的是,这些台词可能是“把保险箱密码告诉我”这样的危险指令。
2. SSTI漏洞原理深度剖析
要真正理解SSTI,咱们得从模板引擎的工作原理说起。我习惯用做菜来比喻:模板文件是菜谱,数据是食材,模板引擎就是厨师。厨师按照菜谱的步骤,把食材加工成菜肴(HTML)。SSTI漏洞就像是有人偷偷修改了菜谱,让厨师在炒菜时顺便把厨房钥匙给出去。
2.1 模板引擎的渲染流程
以Jinja2(Flask默认模板引擎)为例,它的渲染过程可以分为三个阶段:
- 解析阶段:模板引擎读取模板内容,识别其中的特殊语法结构。比如
{ { ... }}表示变量替换,{% ... %}表示控制语句,{# ... #}表示注释。 - 编译阶段:将识别出的模板语法转换成Python的抽象语法树(AST)。
- 执行阶段:结合传入的数据,执行编译后的代码,生成最终的HTML。
当存在SSTI时,攻击者的输入直接进入了第1阶段,被当作模板语法解析。这就好比你在菜谱里写“加入盐{ { 执行系统命令 }}”,厨师不会觉得奇怪,他会真的去执行那个命令。
2.2 漏洞产生的根本原因
根据我多年的审计经验,SSTI漏洞通常出现在以下几种场景:
场景一:动态模板拼接 这是最常见的情况,就像前面那个例子。程序员为了省事,直接把用户输入拼接到模板字符串里。我见过最离谱的代码是这样的:
def render_user_profile(user_input):
# 危险!用户输入直接进入模板
template = """
<div class="profile">
<h2>用户信息</h2>
<p>简介:%s</p>
</div>
""" % user_input
return render_template_string(template)
场景二:模板文件路径可控 有些系统允许用户指定模板文件路径,如果没做好过滤,攻击者可以读取任意文件:
@app.route('/view')
def view_template():
template_name = request.args.get('template', 'default.html')
# 如果template_name是../../../etc/passwd呢?
return render_template(template_name)
场景三:过滤器滥用 Jinja2提供了很多过滤器,比如|safe标记内容为安全,不进行转义。如果错误使用:
<!-- 错误用法 -->
<div>{
{ user_content|safe }}</div>
<!-- 如果user_content包含{
{ 恶意代码 }},就会被执行 -->
场景四:模板继承中的漏洞 模板继承是Jinja2的强大功能,但也可能被滥用:
<!-- base.html -->
{% block content %}{% endblock %}
<!-- 攻击者控制的子模板 -->
{% extends "base.html" %}
{% block content %}
{
{ ''.__class__.__base__.__subclasses__() }}
{% endblock %}
2.3 Python对象的魔法方法
要利用SSTI,必须了解Python的一些特殊属性和方法。这些就像是对象的“后门”,让我们能在模板中访问到原本不该访问的东西。
__class__属性:每个Python对象都有这个属性,它指向对象所属的类。比如''.__class__返回<class 'str'>,[].__class__返回<class 'list'>。
__bases__和__base__:获取类的基类(父类)。''.__class__.__base__返回<class 'object'>,因为所有类最终都继承自object。
__mro__:方法解析顺序,以元组形式返回类的继承链。''.__class__.__mro__返回(<class 'str'>, <class 'object'>)。
__subclasses__()方法:返回类的所有直接子类。这是SSTI利用的关键,因为object是所有类的基类,所以object.__subclasses__()返回当前Python环境中所有已加载的类。
__init__和__globals__:__init__是类的初始化方法,__init__.__globals__返回该函数全局作用域中的所有变量,包括导入的模块。
__builtins__:内置命名空间,包含所有内置函数和异常。通过它可以访问到eval、exec、__import__等危险函数。
我整理了一个关系图,帮你理解这些属性和方法是如何串联起来的:
用户输入 → 模板渲染 → 执行任意代码
↑ ↑ ↑
字符串对象 → 获取类 → 获取基类 → 获取所有子类
↓ ↓ ↓
''.__class__.__base__.__subclasses__() → 寻找危险类 → 调用危险方法

1万+

被折叠的 条评论
为什么被折叠?



