python原型链污染

简介:

python 中的原型链污染是指通过修改对象原型链中的属性,对程序的行为产生以外影响或利用漏洞进行攻击的一种技术

在 Python中,对象的属性和方法可以通过原型链继承来获取。每个对象都有一个原型,原型上定义了对象可以访问的属性和方法。当对象访问属性或方法时,会先在自身查找,如果找不到就会去原型链上的上级对象中查找,原型链污染攻击的思路是通过修改对象原型链中的属性,使得程序在访问属性或方法时得到不符合预期的结果。常见的原型链污染攻击包括修改内置对象的原型、修改全局对象的原型等

这个知识点应用的范围比较小,仅当题目中出现utilsmergePydash(5.1.2)模块中的setset_with函数才会用上

污染条件

需要存在递归合并函数(如merge)且未对特殊属性过滤

merge函数介绍:

1.最简单的 merge 函数(合并字典)

1
2
3
4
def merge(dst, src):
for key, value in src.items():
dst[key] = value
return dst

这个函数只是把 src 字典里的所有键值对复制到 dst 中,如果有相同的键,src 的值会覆盖 dst 的值。

1
2
3
4
5
a = {"name": "Alice", "age": 20}
b = {"age": 21, "city": "Beijing"}

merge(a, b)
print(a) # 输出 {'name': 'Alice', 'age': 21, 'city': 'Beijing'}

效果a 中被添加了 city,并且 age 被更新为 21

2.深度合并(递归合并嵌套字典)

在实际应用中,我们往往希望合并嵌套的字典,而不是简单覆盖。比如:

1
2
3
4
5
6
7
8
9
def deep_merge(dst, src):
for key, value in src.items():
if isinstance(value, dict) and key in dst and isinstance(dst[key], dict):
# 如果都是字典,递归合并
deep_merge(dst[key], value)
else:
# 否则直接赋值(覆盖)
dst[key] = value
return dst

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
config = {
"db": {
"host": "localhost",
"port": 3306
},
"debug": False
}

user_config = {
"db": {
"port": 5432,
"user": "admin"
},
"debug": True
}

deep_merge(config, user_config)
print(config)

输出

text

1
2
3
4
5
6
7
8
{
'db': {
'host': 'localhost',
'port': 5432, # 被覆盖
'user': 'admin' # 新增
},
'debug': True # 被覆盖
}

可以看到,db 字典内部没有被整个替换,而是递归合并:port 更新了,user 被添加了,host 保持不变

merge 在面向对象中的类似行为

如果我们合并的不是字典,而是对象(实例),可以用 setattr 来实现:

1
2
3
def merge_obj(dst, src):
for key, value in src.items():
setattr(dst, key, value)
1
2
3
4
5
6
7
8
9
10
11
class Config:
pass

config = Config()
config.debug = False

user_input = {"debug": True, "db_host": "localhost"}
merge_obj(config, user_input)

print(config.debug) # True
print(config.db_host) # localhost

污染过程分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class father:
secret = "hello"
class son_a(father):
pass
class son_b(father):
pass
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
instance = son_b()

payload = {
"__class__" : {
"__base__" : {
"secret" : "world"
}
}
}

print(son_a.secret)
#hello
print(instance.secret)
#hello
--------------------------------------------------------------------------------------------------------------------------------
merge(payload, instance)
print(son_a.secret)
#world
print(instance.secret)
#world

解释:
payload = {
"__class__" : {
"__base__" : {
"secret" : "world"
}
}
}
这个字典结构设计用于递归修改对象的关键属性:

外层键 "__class__" 指向实例的类对象。

内层键 "__base__" 指向该类的父类。

最内层键 "secret" 是我们要修改的目标属性。

第一次调用:k = "__class__",v = {"__base__": {"secret": "world"}} 即merge(v, getattr(instance, "__class__"))

第二次调用:merge({"__base__": {"secret": "world"}}, son_b)

获取目标类:

上面示例我们是通过__base__属性查找到继承的父类,然后污染到的父类中的secret参数,但是如果目标类与切入点没有父子类继承关系,那我们就无法用__base__属性来进行对目标类的获取和污染

获取全局变量:

在函数或类方法中,我们经常会看到__init__初始化方法,但是它作为类的一个内置方法,在没有被重写作为函数的时候,其数据类型会被当做装饰器,而装饰器的特点就是都具有一个全局属性__globals__属性,__globals__ 属性是函数对象的一个属性,用于访问该函数所在模块的全局命名空间。具体来说就是,__globals__ 属性返回一个字典,里面包含了函数定义时所在模块的全局变量

  • 1.__init__ 是 Python 类中的初始化方法。当你创建一个类的实例时,它会自动被调用,用来设置实例的初始状态。
1
2
3
class MyClass:
def __init__(self, name):
self.name = name
  • 2.在 Python 中,每个函数对象都有一个 __globals__ 属性,它返回一个字典,这个字典包含了该函数定义时所在模块的全局命名空间(即模块级别的所有变量、函数、导入的模块等)
1
2
3
4
5
6
7
8
# 假设文件名为 test.py
x = 100

def foo():
return x

print(foo.__globals__) # 会输出一个字典,里面包含 x、foo 等全局变量
print(foo.__globals__["x"]) # 输出 100

__globals__ 是一个只读字典(但可以修改其中的值,从而影响模块的全局变量)

  • 3.__init__.__globals__ 是指类初始化方法 __init__ 的全局变量字典
1
2
3
4
5
6
7
8
9
10
11
12
13
# 文件 test.py

import os

class MyClass:
def __init__(self):
pass

# 访问 __init__ 的 __globals__

print(MyClass.__init__.__globals__)

# 输出一个字典,包含 'os'、'MyClass' 等全局对象

示例:

1
2
3
4
5
6
7
a=1
def demo():
pass
class A :
def __init__(self):
pass
print(demo.__globals__==globals()==A.__init__.__globals__) #True

我们就可以通过merge加上globals来获得对应的全局变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
a = 1
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
def demo():
pass
class A:
def __init__(self):
pass
class B:
classa = 2

instance = A()
payload = {
"__init__":{
"__globals__":{
"a":4,
"B":{
"classa":5
}
}
}
}
print(B.classa)
print(a)
--------------------------------------------------------------------------------------------------------------------------------
merge(payload, instance)
print(B.classa)
print(a)

污染过程:

第一次调用:merge(payload, instance)

  • 遍历 payload,k = "__init__"v = {"__globals__": {"a": 4, "B": {"classa": 5}}}

    1
    merge(v, getattr(instance, "__init__"))

    merge({"__globals__": {"a": 4, "B": {"classa": 5}}}, instance.__init__)

第二次调用:merge({"__globals__": {"a": 4, "B": {"classa": 5}}}, instance.__init__)

  • src = {"__globals__": {"a": 4, "B": {"classa": 5}}}dst = instance.__init__(函数对象)

    1
    merge(v, getattr(instance.__init__, "__globals__"))

    merge({"a": 4, "B": {"classa": 5}}, instance.__init__.__globals__)

第三次调用:merge({"a": 4, "B": {"classa": 5}}, instance.__init__.__globals__)

  • src = {"a": 4, "B": {"classa": 5}}dst = instance.__init__.__globals__(这是模块的全局命名空间字典
  • 进行污染: a = 4

第四次调用:merge({"classa": 5}, B)

  • src = {"classa": 5}dst = B(类对象)
  • 进行污染:B.classa = 5

污染结果:

获取其他模块:

在全局变量的前提下,是我们都在入口文件中的类对象或者属性来进行操作的,但是如果我们操作的位置在入口文件中,而目标对象并不在入口文件当中,这时候我们就需要对其他加载过的模块来获取了

import加载获取:

适用情况:*

跨模块污染

当目标程序使用多个模块时,通过污染一个模块的 __globals__,可以影响到其他已导入的模块

适用场景

  • 程序将配置、工具函数、常量等放在独立模块中
  • 攻击者可以通过污染主模块的全局字典,修改这些模块的内容

在简单的关系情况下,我们可以直接通过import来进行加载,在payload中我们只需要对对应的模块重新定位就可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import demo
payload = {
"__init__":{
"__globals__":{
"demo":{
"a":4,
"B":{
"classa":5
}
}
}
}
}
##demo.py
a = 1
class B:
classa = 2

sys模块加载获取:

在很多环境当中,会引用第三方模块或者是内置模块,而不是简单的import同级文件下面的目录,所以我们就要借助sys模块中的module属性,这个属性能够加载出来在自运行开始所有已加载的模块,从而我们能够从属性中获取到我们想要污染的目标模块:

同样是刚才的情景,因为我们已经加载过demo.py了,所以我们用sys来对里面的目标进行获取,但是存在一个问题就是,我们的payload传参的时候大概率是在它源码已有的基础上进行传参,很有可能源码中没有引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sys
payload = {
"__init__":{
"__globals__":{
"sys":{
"modules":{
"demo":{
"a":4,
"B":{
"classa":5
}
}
}
}
}
}
}

加载器loader获取:

loader加载器在python中的作用是为实现模块加载而设计的类,其在importlib这一内置模块中有具体实现。而importlib模块下所有的py文件中均引入了sys模块,这样我们和上面的sys模块获取已加载模块就联系起来了,所以我们的目标就变成了只要获取了加载器loader,我们就可以通过loader.__init__.__globals__['sys']来获取到sys模块,然后再获取到我们想要的模块。

那么我们现在的目标就变成了获取loader:

在Python中,__loader__是一个内置的属性,包含了加载模块的loader对象,Loader对象负责创建模块对象,通过__loader__属性,我们可以获取到加载特定模块的loader对象。

1
2
3
4
5
import math
# 获取模块的loader
loader = math.__loader__
# 打印loader信息
print(loader)

在这个例子当中我们就能够明白,math模块的__loader__属性包含了一个loader对象,负责加载math模块

在python中还存在一个__spec__,包含了关于类加载时候的信息,他定义在Lib/importlib/_bootstrap.py的类ModuleSpec,所以可以直接采用<模块名>.__spec__.__init__.__globals__['sys']获取到sys模块