Python原型链污染

文章发布时间:

最后更新时间:

文章总字数:
1.3k

预计阅读时间:
4 分钟

Python原型链污染

这个知识点应用的范围比较小,仅当题目中出现utilsmergePydash模块中的setset_with函数才会用上。今天来学习其实是因为DASCTF的第一题ezflask里面出现了。

Python类与继承

首先经典回顾一下python的类与继承:

  • 在Python中,定义类是通过class关键字,class后面紧接着是类名,紧接着是(object),表示该类是从哪个类继承下来的,所有类的本源都是object类
  • 可以自由地给一个实例变量绑定属性,像js
  • 由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把类内置的属性绑上
  • 注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
  • 当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到
  • 判断一个变量是否是某个类型可以用isinstance()判断。

Python原型链污染

常规利用(继承关系)

其实python的原型链污染和nodejs的原型链污染很像,一样是通过给父类属性赋值使得每一个子类或者每一个实例都带有这样的值,也一样是通过merge函数:

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
class father():
secret = "haha"


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)

sonB = son_b()
payload = {
"__class__":{
"__base__":{
"secret":"polluted!!"
}
}
}
merge(payload,sonB)
sonA = son_a()
print(sonA.secret) #polluted!!

通过断点调试可以看出这个merge函数在走到hasattr处,由于我们的payload是一层字典套一层字典,就会递归调用merge,并且由于getattr(dst,k),dst就在一直按着payload的键发生变化,从到类,再到父类,最后把父类的secret赋值为polluted,成功实现了原型链污染。

payload也很好理解,其实就是利用了python的链式继承关系,最后找到这个类即可,和SSTI通过链式继承关系找os模块很像。

类的内置属性,如__str__也可以被污染,但是需要注意,并不是所有类的属性都可以被污染,比如Object就无法被污染。

拓展利用(无需继承)

上述操作需要类之间有继承关系才能通过修改父类来对子类产生影响,比较鸡肋,下面的操作只要定位到了类就能直接操作。不过我在想:这也许已经变成了单纯的污染,和原型链已经没什么关系了罢(笑)

Python中,函数或类方法(对于类的内置方法如__init__这些来说,内置方法在并未重写时其数据类型为装饰器即wrapper_descriptor,只有在重写后才是函数function,这也就解释了为什么在SSTI中不是每一个类的__init__都有__globals__属性)均具有一个__globals__属性,该属性将函数或类方法所申明的变量空间中的全局变量以字典的形式返回(相当于这个变量空间中的globals函数的返回值)

1
2
3
4
5
6
7
8
9
def test():
pass

class a:
def __init__(self): #注意如果不重写__init__就没有__globals__属性
pass

print(test.__globals__ == globals() == a.__init__.__globals__)
#True

也就是说我们找到了__globals__就能找到所有的全局变量和所有的类,进而进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class son_a():
secret_var = "haha"

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

sonA = son_a()
sonB = son_b()
payload = {
"__init__":{
"__globals__":{
"sonA":{
"secret_var":"polluted"
}
}
}
}
merge(payload,sonB)
print(sonA.secret_var) #polluted 借由global可修改到没有继承关系的类或者修改到全局变量,例题dasctf ezflask 修改__file__直接看flag

实际环境中往往是多层模块导入,甚至是存在于内置模块或三方模块中导入,这个时候通过直接看代码文件中import语法查找就十分困难,而解决方法则是利用sys模块

sys模块的modules属性以字典的形式包含了程序自开始运行时所有已加载过的模块,可以直接从该属性中获取到目标模块

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

以下是python原型链污染的一些重点关照对象

  • 函数形参默认值替换:函数的__defaults____kwdefaults__这两个内置属性
  • os.environ赋值
  • flask secret_key修改
  • 修改当前展示目录或者展示文件(DASCTF修改__file__

参考链接:

Python原型链污染变体(prototype-pollution-in-python) - 跳跳糖 (tttang.com)