初探Python的原型链污染攻击(prototype-pollution-in-python)

代码展示

合并示例

在实施原型链污染攻击时需要一个数值合并函数将特定的值污染到类的属性之中,一个标准的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict: //判断检查dst中是否存在键k,且对应的值是一个字典
merge(v, dst.get(k)) //合并
else:
dst[k] = v //存在键k;但是对应的值不是字典,那么直接赋值
elif hasattr(dst, k) and type(v) == dict: //不存在键k;但是存在与k同名的属性
merge(v, getattr(dst, k)) //将src中的值与dst进行合并
else:
setattr(dst, k, v)
//如果目标字典 dst 中既没有对应的键 k,也没有与键 k 同名的属性,则直接使用 setattr 将属性 k 设置为值 v。

代码审计

此时是先定义了一个名为meger的函数,此时这个函数只接受src和dst两个参数,此时我们让他们代表源字典和目标字典。接下来使用一个for循环来遍历src字典的键值;k对应其中的键,v对应其中的值。接下来使用一个if判断,这个条件判断语句检查目标字典是否存在键k,并且对应的值是一个字典。此时若是dst中已经存在键k,且对应的是一个字典,则递归调用meger函数将v(源字典中的值)与dst.get(k)(目标字典中对应键的值)合并。
如果 dst 中已经存在键 k,但对应的值不是字典,那么直接将 src 中的值v赋给目标字典dst的键k。如果目标字典dst中没有对应的键k,但是存在与键k
同名的属性,则进入这个条件判断。在这种情况下,同样进行递归合并。将 v(源字典中的值)与 getattr(dst, k)(目标字典中对应属性的值)合并如果目标
字典dst中既没有对应的键 k,也没有与键k同名的属性,则直接使用setattr将属性k设置为值v。
总而言之,这个函数会将src字典中的所有键值合并到dst字典中,或者直接在dst字典中创建新的键值。目的是让dst字典包含src字典的所有数据

污染示例

那么此时由于python中的类会继承父类中的属性,而类中声明的属性是唯一的,所有此时我们的目标就是这些在多个类、示例中仍然指向唯一的属性,比如在自定义属性以及__开头的内置属性

自定义属性

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
class father:
secret = "haha" //基类;其中有一个类属性为secret,值为haha

class son_a(father):
pass

class son_b(father):
pass
//son_a和son_b是继承father的子类,因为没有额外的定义;所以继承father的所有属性和方法

def merge(src, dst):
# Recursive merge function
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)
//递归合并函数,将dst字典内的数据合并至sec内

instance = son_b()
payload = {
"__class__" : {
"__base__" : {
"secret" : "no way"
}
}
}

//此时创建了一个son_b类的实例instance;定义了一个字典payload,其中包含了特殊的结构,用于在合并时更改instance对象的属性

print(son_a.secret)
#haha
//输出haha是因为son_a和son_b共享secret的属性
print(instance.secret)
#haha
//输出haha是因为instance是son_b的实例,它继承了father的属性
merge(payload, instance)
//此时对instance进行合并,根据payload的定义,将instance的secret属性从原来的haha改为no way
print(son_a.secret)
#no way
//因为 son_a 类和 father 类共享 secret 属性,它们都受到了合并的影响。
print(instance.secret)
#no way
//因为在合并过程中,instance 的 secret 属性被修改为 "no way"

修改内置属性

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
class father:
pass //基类且没有额外的定义

class son_a(father):
pass

class son_b(father):
pass
//都为father的子类且没有额外的定义;继承了father类的所有属性和方法

def merge(src, dst):
# Recursive merge function
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__" : {
"__str__" : "Polluted ~"
}
}
}

//创建了一个son_b类的实例instance;并定义了一个字典payload,其中包含了一个特殊字符,用于在合并的时候修改属性。这里使用__class__键来指定要修改的类,然后在__base__下定义要修改的属性名和对应的值

print(father.__str__)
#<slot wrapper '__str__' of 'object' objects>
merge(payload, instance)
print(father.__str__)
#Polluted ~

无法污染的Object

正如前面所诉,并不是所有的类属性都可以被污染,比如Object的属性就无法被污染,所以此时我们需要的是目标类可以被切入或者对象可以通过属性值查找获得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def merge(src, dst):
# Recursive merge function
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)

payload = {
"__class__" : {
"__str__" : "Polluted ~"
}
}

merge(payload, object)
#TypeError: can't set attributes of built-in/extension type 'object'

利用

更广泛的获取

在代码展示部分所给出的例子中都是通过__base__属性来找到其中继承的父类,但是如果目标类与切入点类或者实例没有继承关系的话那么此时的这种
方法就会显得十分的无力

全局变量的获取

在python中,函数或者类方法
(对于类的内置方法比如__init__这些来说,内置方法在并未重写时其数据类型为装饰器wrapperdescripptor,只有在重写之后才是函数function)
此时均具有__globals__属性,改属性将函数或者类方法所申明的变量空间的全局变量通过字典的形式返回

1
2
3
4
5
6
7
8
9
10
11
secret_var = 114  //全局变量

def test(): //空函数
pass

class a: //自定义类a
def __init__(self): //有一个构造函数__init__
pass //pass语句表示不执行任何操作

print(test.__globals__ == globals() == a.__init__.__globals__)
#True

此时我们将注意力集中在这段语句

1.
test.__globals__ 返回 test函数的全局命名空间,即在test函数中定义的全局变量和函数的字典。在这里,test函数中并没有定义任何全局变量或函数,所以返回的是全局作用域。
2.
globals()返回当前全局作用域的字典,包含了所有在全局范围内定义的变量和函数,包括secret_var和test()函数。
3.
a.__init__.__globals__返回 a 类的构造函数__init__的全局命名空间,和test.__globals__一样,因为在__init__中也没有定义全局变量或函数,所以也返回的是全局作用域globals()。
所以此时我们就可以利用__globlasl__来获取全局变量,这样就不需要通过__base__属性来寻找父子关系了;直接修改无继承关系的类属性甚至全局变量

实例

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
secret_var = 114

def test():
pass

class a:
secret_class_var = "secret"

class b:
def __init__(self):
pass

def merge(src, dst):
# Recursive merge function
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 = b()

payload = {
"__init__" : {
"__globals__" : {
"secret_var" : 514,
"a" : {
"secret_class_var" : "Pooooluted ~"
}
}
}
}

print(a.secret_class_var)
#secret
print(secret_var)
#114
merge(payload, instance)
print(a.secret_class_var)
#Pooooluted ~
print(secret_var)
#514

已加载模块的获取

局限于当前模块的全局变量获取显然不够,很多情况下需要对并不是定义在入口文件的类对象或者属性,而我们的操作位置又在入口文件中,这时候就需要对其他加载过的的模块来获取

加载关系简单

在加载关系简单的情况下我们可以直接从文件的import语法找到目标模块,这时候我们就可以直接通过获取全局变量来得到目标模块

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
### test.py
import test_1

class cls:
def __init__(self):
pass

def merge(src, dst):
# Recursive merge function
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 = cls()

payload = {
"__init__" : {
"__globals__" : {
"test_1" : {
"secret_var" : 514,
"target_class" : {
"secret_class_var" : "Poluuuuuuted ~"
}
}
}
}
}

print(test_1.secret_var)
#secret
print(test_1.target_class.secret_class_var)
#114
merge(payload, instance)
print(test_1.secret_var)
#514
print(test_1.target_class.secret_class_var)
#Poluuuuuuted ~

加载关系复杂

如CTF题目等实际环境中往往是多层模块导入,甚至是存在于内置模块或三方模块中导入,这个时候通过直接看代码文件中import语法查找就十分困难,而解决方法则是利用sys模块。
sys模块的modules属性以字典的形式包含了程序自开始运行时所有已加载过的模块,可以直接从该属性中获取到目标模块

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
#test.py

import test_1
import sys

class cls:
def __init__(self):
pass

def merge(src, dst):
# Recursive merge function
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 = cls()

payload = {
"__init__" : {
"__globals__" : {
"sys" : {
"modules" : {
"test_1" : {
"secret_var" : 514,
"target_class" : {
"secret_class_var" : "Poluuuuuuted ~"
}
}
}
}
}
}
}

print(test_1.secret_var)
#secret
print(test_1.target_class.secret_class_var)
#114
merge(payload, instance)
print(test_1.secret_var)
#514
print(test_1.target_class.secret_class_var)
#Poluuuuuuted~

当然我们去使用的Payload绝大部分情况下是不会这样的,如上的Payload实际上是在已经import sys的情况下使用的,而大部分情况是没有直接导入的,这样问题就从寻找import特定模块的语句转换为寻找import的sys模块的语句,对问题解决的并不见得有多少优化

加载关系复杂的实际使用

为了进一步的优化,这里采用的方式是利用python中的加载器loader;这个简单来说就是为了实现模块加载而设计的类;其在importable这一内置模块中有具体的实现,并且在importlib模块下的py文件引入了sys模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
print("sys" in dir(__import__("importlib.__init__")))
#True
print("sys" in dir(__import__("importlib._bootstrap")))
#True
print("sys" in dir(__import__("importlib._bootstrap_external")))
#True
print("sys" in dir(__import__("importlib._common")))
#True
print("sys" in dir(__import__("importlib.abc")))
#True
print("sys" in dir(__import__("importlib.machinery")))
#True
print("sys" in dir(__import__("importlib.metadata")))
#True
print("sys" in dir(__import__("importlib.resources")))
#True
print("sys" in dir(__import__("importlib.util")))
#True

所以此时我们只需要能获取一个loader便可以使用loader.__init__.__globals__['sys']的方式拿到sys模块,这样进而获取目标模块。

loader的获取

1
2
1.<模块名>.__spec__.__init__.__globals__['sys']获取到sys模块
2.<模块名>.__spec__.loader.__init__.__globals__['sys']