python原型链污染
这个知识点应用的范围比较小,仅当题目中出现utils
的merge
或Pydash
模块中的set
和set_with
函数才会用上。dasctf考了好多次,就来学习下
前置知识
回顾一下python的类与继承
在Python中,定义类是通过class关键字,class后面紧接着是类名,紧接着是(object),表示该类是从哪个类继承下来的,所有类的本源都是object类
可以自由地给一个实例变量绑定属性,像js
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__
方法,在创建实例的时候,就把类内置的属性绑上
注意到__init__
方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__
方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到
判断一个变量是否是某个类型可以用isinstance()
判断
通过继承关系的利用
python的原型链污染和nodejs的原型链污染很像,一样是通过给父类属性赋值使得每一个子类或者每一个实例都带有这样的值,也一样是通过merge函数:
1 | class father(): |
通过断点调试可以看出这个merge函数在走到hasattr处,由于我们的payload是一层字典套一层字典,就会递归调用merge,并且由于getattr(dst,k),dst就在一直按着payload的键发生变化,从到类,再到父类,最后把父类的secret赋值为polluted,成功实现了原型链污染
payload也很好理解,其实就是利用了python的链式继承关系,最后找到这个类即可,和SSTI通过链式继承关系找os模块很像
类的内置属性,如__str__
也可以被污染,但是需要注意,并不是所有类的属性都可以被污染,比如Object就无法被污染
无需继承的利用
这个方法是在Pazuris师傅的博客看到的,不怎么理解
上述操作需要类之间有继承关系才能通过修改父类来对子类产生影响,比较鸡肋,下面的操作只要定位到了类就能直接操作
在Python中,对于类的内置方法如__init__
这些来说,内置方法在并未重写时其数据类型为装饰器即wrapper_descriptor
,只有在重写后才是函数function
,这也就解释了为什么在SSTI中不是每一个类的__init__
都有__globals__
属性。函数或类方法均具有一个globals属性,该属性将函数或类方法所申明的变量空间中的全局变量以字典的形式返回(相当于这个变量空间中的globals函数的返回值)
1 | def test(): |
也就是说我们找到了__globals__
就能找到所有的全局变量和所有的类,进而进行操作
1 | class son_a(): |
实际环境中往往是多层模块导入,甚至是存在于内置模块或三方模块中导入,这个时候通过直接看代码文件中import语法查找就十分困难,而解决方法则是利用sys模块
sys模块的modules属性以字典的形式包含了程序自开始运行时所有已加载过的模块,可以直接从该属性中获取到目标模块
获取sys:<模块名>.__spec__.loader.__init__.__globals__['sys']
以下是python原型链污染的一些重点关照对象
- 函数形参默认值替换:函数的
__defaults__
和__kwdefaults__
这两个内置属性 - os.environ赋值
- flask secret_key修改
- 修改当前展示目录或者展示文件(DASCTF修改
__file__
)
参考
Python原型链污染
Python原型链污染变体(prototype-pollution-in-python) - 跳跳糖