python原型链污染
python原型链污染
简介:
python 中的原型链污染是指通过修改对象原型链中的属性,对程序的行为产生以外影响或利用漏洞进行攻击的一种技术
在 Python中,对象的属性和方法可以通过原型链继承来获取。每个对象都有一个原型,原型上定义了对象可以访问的属性和方法。当对象访问属性或方法时,会先在自身查找,如果找不到就会去原型链上的上级对象中查找,原型链污染攻击的思路是通过修改对象原型链中的属性,使得程序在访问属性或方法时得到不符合预期的结果。常见的原型链污染攻击包括修改内置对象的原型、修改全局对象的原型等
这个知识点应用的范围比较小,仅当题目中出现utils的merge或Pydash(5.1.2)模块中的set和set_with函数才会用上
污染条件
需要存在递归合并函数(如merge)且未对特殊属性过滤
merge函数介绍:
1.最简单的 merge 函数(合并字典)
1 | def merge(dst, src): |
这个函数只是把 src 字典里的所有键值对复制到 dst 中,如果有相同的键,src 的值会覆盖 dst 的值。
1 | a = {"name": "Alice", "age": 20} |
效果:a 中被添加了 city,并且 age 被更新为 21
2.深度合并(递归合并嵌套字典)
在实际应用中,我们往往希望合并嵌套的字典,而不是简单覆盖。比如:
1 | def deep_merge(dst, src): |
例子:
1 | config = { |
输出:
text
1 | { |
可以看到,db 字典内部没有被整个替换,而是递归合并:port 更新了,user 被添加了,host 保持不变
merge 在面向对象中的类似行为
如果我们合并的不是字典,而是对象(实例),可以用 setattr 来实现:
1 | def merge_obj(dst, src): |
1 | class Config: |
污染过程分析:
1 | class father: |
获取目标类:
上面示例我们是通过__base__属性查找到继承的父类,然后污染到的父类中的secret参数,但是如果目标类与切入点没有父子类继承关系,那我们就无法用__base__属性来进行对目标类的获取和污染
获取全局变量:
在函数或类方法中,我们经常会看到__init__初始化方法,但是它作为类的一个内置方法,在没有被重写作为函数的时候,其数据类型会被当做装饰器,而装饰器的特点就是都具有一个全局属性__globals__属性,__globals__ 属性是函数对象的一个属性,用于访问该函数所在模块的全局命名空间。具体来说就是,__globals__ 属性返回一个字典,里面包含了函数定义时所在模块的全局变量
- 1.
__init__是 Python 类中的初始化方法。当你创建一个类的实例时,它会自动被调用,用来设置实例的初始状态。
1 | class MyClass: |
- 2.在 Python 中,每个函数对象都有一个
__globals__属性,它返回一个字典,这个字典包含了该函数定义时所在模块的全局命名空间(即模块级别的所有变量、函数、导入的模块等)
1 | # 假设文件名为 test.py |
__globals__ 是一个只读字典(但可以修改其中的值,从而影响模块的全局变量)
- 3.
__init__.__globals__是指类初始化方法__init__的全局变量字典
1 | # 文件 test.py |
示例:
1 | a=1 |
我们就可以通过merge加上globals来获得对应的全局变量:
1 | a = 1 |
污染过程:
第一次调用: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 | import demo |
sys模块加载获取:
在很多环境当中,会引用第三方模块或者是内置模块,而不是简单的import同级文件下面的目录,所以我们就要借助sys模块中的module属性,这个属性能够加载出来在自运行开始所有已加载的模块,从而我们能够从属性中获取到我们想要污染的目标模块:
同样是刚才的情景,因为我们已经加载过demo.py了,所以我们用sys来对里面的目标进行获取,但是存在一个问题就是,我们的payload传参的时候大概率是在它源码已有的基础上进行传参,很有可能源码中没有引入。
1 | import sys |
加载器loader获取:
loader加载器在python中的作用是为实现模块加载而设计的类,其在importlib这一内置模块中有具体实现。而importlib模块下所有的py文件中均引入了sys模块,这样我们和上面的sys模块获取已加载模块就联系起来了,所以我们的目标就变成了只要获取了加载器loader,我们就可以通过loader.__init__.__globals__['sys']来获取到sys模块,然后再获取到我们想要的模块。
那么我们现在的目标就变成了获取loader:
在Python中,__loader__是一个内置的属性,包含了加载模块的loader对象,Loader对象负责创建模块对象,通过__loader__属性,我们可以获取到加载特定模块的loader对象。
1 | import math |
在这个例子当中我们就能够明白,math模块的__loader__属性包含了一个loader对象,负责加载math模块
在python中还存在一个__spec__,包含了关于类加载时候的信息,他定义在Lib/importlib/_bootstrap.py的类ModuleSpec,所以可以直接采用<模块名>.__spec__.__init__.__globals__['sys']获取到sys模块