pickle反序列化随记
1.关于pickle和Cpickle
等同于PHP的serialize和unserialize,是一个序列化和反序列化的过程。
2.pickle序列化和反序列化相关的函数
4个,dump,dumps,load,loads
其中dump和load一组,dumps和loads一组
dump&load:dump用于序列化一个对象,存入一个文件中,相应的,load用于反序列化,但是需要从dump生成的文件中进行读取数据
dumps&loads:序列化后直接返回一个序列化bytes对象,相应的,loads直接从bytes对象反序列化而不需要从文件读取数据
以上就是dumps&loads和dump&load的区别
我们写一个测试程序,来体验序列化与反序列化的过程
3.Pickle Virtual Machine PVM
作用:Python解释器能够将源代码转换成字节码,编译后的字节码就交由Python虚拟机进行运行
简单说PVM就是一个运行字节码的引擎
执行过程:
Step1:源代码编译成字节码
字节码是Python独有的东西,它需要进一步编译才能被机器执行。
如果进程在主机上有写入权限,则Python会把程序的字节码以一个.pyc后缀的文件保存
若没有权限,则在内存中生成字节码,程序运行结束后丢弃
Step2:把编译好的字节码发送到PVM,PVM循环迭代执行字节码指令直到操作完成
4.Pickle模块本质是一个轻量级PVM
Pickle本身是一门基于栈的语言
三部分组成:指令处理器,栈区,标签区(Instruction processer,stack,memo)
指令处理器:用于从数据流里读取操作码和参数,然后执行,其中执行结果会不断改变stack和memo区的值。
遇到.符号,即结束符号,操作结束。此时栈顶值作为反序列化对象返回
栈区:数据暂存区域,在不断地进出栈过程中完成反序列化操作。最终结果在栈顶生成
标签区:数据索引或标记区域,提供反序列化过程的存储功能,即把反序列化完成的数据以关键值形式储存与memo区域,以便后面调用
操作码:常用有如下:
c:读取本行内容作为模块名(module),下一行内容作为对象名(object),然后把module.object整体作为对象压入栈中
(:左括号,把一个标记对象(MARK)压入栈中,确定命令执行的位置,和t一起用可以产生元组
(热知识:Python元组:元组与列表类似,元组的元素不能修改,元组采用(),列表采用[])
S:后跟字符串(String),PVM读取引号中内容,直到遇到换行符,然后压入栈中。例如S'string1'
t:从栈中弹出数据,弹射顺序与压栈顺序相同的,直到弹出左括号,此时形成元组,该元组会被压入栈中。
大概是这么一个流程
R:把之前压入栈中的元组和所有可调用对象全部取出,然后将该元组作为可调用参数的对象,执行它,执行后结果压入栈中
.:结束反序列化过程
p:将栈顶数据储存于memo区,附上编号,比如p0,即把栈顶数据存在memo区的0号位
d:在栈顶创建一个字典,把memo中的内容转换成键值并储存于该字典中,然后把这个字典存储于memo中
V:向栈顶压入一个unicode字符串
有时候操作码会一起用,比如Rp4就代表着,先进行R操作,顺便储存到memo这个意思
序列化过程可以抽象成三步
Step1:从对象提取属性
Step2:写入对象的模块名,类名(c操作码)
Step3:写入对象所有属性的键值对
而反序列化就是把以上三步反过来
5.pickle反序列化漏洞成因
问题在__reduce()__函数上,这个函数会在反序列化流程结束后自动调用,类似于PHP反序列化中的__wakeup()这个魔术方法
该函数调用时,返回一个元组,第一个元素是一个可调用对象,这个对象会在创建对象的时候就调用
第二个元素时可调用对象的参数,同为元组。
这个过程和R操作码的作用时类似的,而实际上R操作码就是该函数的底层实现
反序列化进程结束,Python自动调用此函数,如果控制了被调用函数的参数,就可以恶意执行代码。
漏洞成因就是这里
6.漏洞利用
在网络传输信息时利用了pickle函数的地方,且可以人为传参的情况
源代码:
import base64
import pickle
from flask import Flask, Response, request, render_template
import Hed9eh0g_girlfriend
app = Flask(name)
class girl:
def init(self, name, age):
self.name = name
self.age = age
@app.route('/', methods=["GET"])
def hello():
return render_template('index.html')
@app.route('/guess', methods=['GET'])
def index():
if request.method == 'GET':
try:
pickle_data = request.args.get('payload')
print(pickle_data)
she = pickle.loads(base64.b64decode(pickle_data))
if she.name == Hed9eh0g_girlfriend.name and she.age == Hed9eh0g_girlfriend.age:
return Hed9eh0g_girlfriend.flag
except Exception as e:
print(repr(e))
return "Something wrong"
def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()
@app.route('/source', methods=['GET'])
def show_source():
return Response(read(file), mimetype='text/plain')
if name == 'main':
app.run(host='0.0.0.0', port=8081)
目的:在/guess页面上通过传入payload使得she.name == Hed9eh0g_girlfriend.name and she.age == Hed9eh0g_girlfriend.age
做法
Step1:先写一段例子序列化,这一步目的是要拿到序列化字符串的格式,以便构造payload
Step2:构造payload
此处的思路是:如果我们直接把我们写的名字和年龄改成对应的名字和年龄,我们就能够使条件成立
但是我们并不知道girl的名字和年龄,所以这条路行不通
我们的思路是利用c操作符,直接把Hed9eh0g_girlfriend.name和Hed9eh0g_girlfriend.age传进去
这样得到payload,然后进行base64加密,传入/guess页面,就好了
把这一段用pickletools.optimize化简q操作符之后再改动传参也是可以的
写的python:
运行结果: