共计 2224 个字符,预计需要花费 6 分钟才能阅读完成。
之前看过许多有关Python原型链污染的题,自己一道也没能做出。
于是今天下定决心好好把这个问题解决一下
1.什么是Python原型链污染?
Python原型链污染是一种通过修改对象原型链中的属性,导致程序行为偏离预期的攻击技术。其核心原理与JavaScript原型链污染类似,但实现方式因语言特性而有所差异。
- 原型继承特性
Python中每个对象通过__class__
属性指向其所属类,类通过__base__
属性指向父类。当访问对象属性时,若当前对象/类中未定义,会沿原型链向上查找14。 - 污染条件
需要存在递归合并函数(如merge
)且未对特殊属性过滤
接下来我们来看一个最简单的原型链污染
class Config:
is_admin = False
def set_config(cls, key, value):
setattr(cls, key, value)
def get_config(cls, key):
return getattr(cls, key, None)
instance=Config()
print(instance.is_admin) #False
setattr(instance,'is_admin','True')
print(instance.is_admin) #True
进入python的debug模式,给倒数三行打上断点。我们可以看到刚初始化的instance里面的is_admin为false

经过setattr函数后is_admin变成了true

这个就是最简单的原型链污染
接下来我们看看一段示例代码
from flask import Flask, request, jsonify
from config import Config
app = Flask(__name__)
@app.route('/update_config',</span> methods=['POST'])
def update_config():
data = request.json
for key, value in data.items():
Config.set_config(key, value)
return jsonify({"status": "success", "config": data})
@app.route('/check_admin',</span> methods=['GET'])
def check_admin():
is_admin = Config.get_config('is_admin')
return jsonify({"is_admin": is_admin})
if __name__ == '__main__':
app.run(debug=True)
如果传入恶意参数将is_admin污染为true则可以实现对管理员校验的绕过
因为没有对传入参数进行校验我们可以试图传入{“is_admin”:True}
setattr()函数就会把is_admin参数污染为true
实战中我们更经常看到的是merge的合并函数
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)#hello
print(son_a.secret)#hello
print(instance.secret)#hello
这里由于个人觉得这个函数比较绕,我们打上断点来一步一步的解析这个函数

刚进入循环时候对应的值,整个阶段我们注意下dst变量的变化

此时dst还是son_b
在经过判断语句后(进入elif分支,因为dst没有__getitem__属性)
此时我们将第二次进入merge函数

进入后:

我们发现dst变成了son_b.__class__而src则是{‘base‘: {‘secret’: ‘world’}}
这是因为getattr函数获取了son_b的class属性,也就是son_b.__class__
src则是之前的v
继续进行分支的判断,我们即将第三次进入merge函数

进入后:

和之前类似,获取了son_b的class的base属性,相当于son_b.__class__.__base__也就是father类
第三次循环的k和v

此时的v不是字典了,所以进入了else分支

到了setattr函数了
此时dst为father,k为secret,v为world
所以我们将father类的secret属性成功污染为了world