什么是pickle
pickle是实现python序列化和反序列化操作的模块之一。pickle可以看作是一种独立的栈语言,它由一串串opcode(指令集)组成。该语言的解析是依靠Pickle Virtual Machine (PVM)进行的。
pickle序列化和反序列化的分析
1 | import pickle |
序列化分析:
通过dumps()方法对类的实例化对象进行序列化,序列化出来的格式是二进制字节流的。
内容包括:1,类的定义信息(Myclass这个类来自__main__模块) 2,序列化对象属性的信息
反序列化过程解析:
反序列化的内容就是一个Myclass的一个实例化对象后面带一个地址
过程:1,找到类的定义 2,重新创造一个类的实例对象(这一点和php的有所不同,php是恢复对象) 3,恢复这个对象的属性 4,返回这个对象
注意反序列的过程这里涉及到后面的原理
__reduce__魔术方法
该魔术方法期待return一个字符串或者最好是一个元组
1 | def reduce(self): |
callable:可调用对象(如exec,os. system,或一个类名)。
args_tuple:传给callable的参数(必须是一个元组)。
可以把返回值理解成PHP中call_user_func函数的运用,第一个参数就是需要传入的类或者函数对象,后面的元组是需要传入该函数的参数
1 | import pickle |
其实在序列化的时候__reduce__方法就触发了,但是反序列化才是触发命令执行的。而且你看序列化出来的二进制字节都没有了对象的属性,而是__reduce__这个方法的返回值。
1 | 当你定义了__reduce__这个方法的时候,pickle会优先使用它的返回值来构建一个对象的数据流, |
pickle反序列化漏洞成因
- Pickle数据在反序列化时自动执行Python字节码(或对象方法)
- 模块本质上不会对反序列化的内容进行校验
所以正因为这两点pickle在反序列化时就会自动执行字节码,从而导致任意命令执行。
那么就又有疑问了?为什么Pickle数据在反序列化时自动执行Python字节码(或对象方法)?
这才是反序列化漏洞的底层原理
还记得上面说的反序列化的流程吗?重构对象,那么它是如何重构对象的呢?
重构对象意味着 pickle 不仅仅是简单地读取数据,它还会执行代码来“重建”一个完整的 Python 对象,包括它的类型、属性,甚至是初始化和方法调用。
先看最上面的那个demo吧!它反序列化的二进制字节流是:
b’\x80\x04\x95M\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x07Myclass\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x06rufeii\x94\x8c\x03age\x94K\x13\x8c\x08language\x94\x8c\x07Chinese\x94ub.’
那么按照流程就是先先找到类的定义去重新创造一个对象 -> 对应执行的代码是:Myclass()
然后在恢复属性 -> 执行的是:name = ‘rufeii’这种
最后在返回这个对象。
那么对于__reduce__那个demo,在恢复对象的时候执行了对象的方法,从而触发漏洞。
opcode利用
常用的opcode
| 指令 | 描述 | 具体写法 | 栈上的变化 |
|---|---|---|---|
| c | 获取一个全局对象或import一个模块 | c[module]\n[instance]\n | 获得的对象入栈 |
| o | 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) | o | 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 |
| i | 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) | i[module]\n[callable]\n | 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 |
| N | 实例化一个None | N | 获得的对象入栈 |
| S | 实例化一个字符串对象 | S’xxx’\n(也可以使用双引号、'等python字符串形式) | 获得的对象入栈 |
| V | 实例化一个UNICODE字符串对象 | Vxxx\n | 获得的对象入栈 |
| I | 实例化一个int对象 | Ixxx\n | 获得的对象入栈 |
| F | 实例化一个float对象 | Fx.x\n | 获得的对象入栈 |
| R | 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R | 函数和参数出栈,函数的返回值入栈 |
| . | 程序结束,栈顶的一个元素作为pickle.loads()的返回值 | . | 无 |
| ( | 向栈中压入一个MARK标记 | ( | MARK标记入栈 |
| t | 寻找栈中的上一个MARK,并组合之间的数据为元组 | t | MARK标记以及被组合的数据出栈,获得的对象入栈 |
| ) | 向栈中直接压入一个空元组 | ) | 空元组入栈 |
| l | 寻找栈中的上一个MARK,并组合之间的数据为列表 | l | MARK标记以及被组合的数据出栈,获得的对象入栈 |
| ] | 向栈中直接压入一个空列表 | ] | 空列表入栈 |
| d | 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) | d | MARK标记以及被组合的数据出栈,获得的对象入栈 |
| } | 向栈中直接压入一个空字典 | } | 空字典入栈 |
| p | 将栈顶对象储存至memo_n | pn\n | 无 |
| g | 将memo_n的对象压栈 | gn\n | 对象被压栈 |
| 0 | 丢弃栈顶对象 | 0 | 栈顶对象被丢弃 |
| b | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b | 栈上第一个元素出栈 |
| s | 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 | s | 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 |
| u | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u | MARK标记以及被组合的数据出栈,字典被更新 |
| a | 将栈的第一个元素append到第二个元素(列表)中 | a | 栈顶元素出栈,第二个元素(列表)被更新 |
| e | 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 | e | MARK标记以及被组合的数据出栈,列表被更新 |
命令执行
主要是这三个R\i\o
1 | # R |
具体要结合opcode的含义来理解
变量覆盖
1 | import pickle |