绕过 RestrictedUnpickler

上周跑模型的间隙,看到一个关于 Python Pickle 反序列化的 CTF 题,和以往见到的不太一样,感觉很有意思,遂打算研究一下。

0x00 Pickle 反序列化

我们知道,当我们控制了如 pickle.loads等序列化数据的输入接口时,我们可以通过构造恶意的 Payload 来达到任意代码执行的效果。这是因为, pickle 库在反序列化(unpickling)的过程中,默认会导入 Payload 中找到的任意类或者函数对象。所以,Python 的开发者也一直警告大家,不要轻易用 pickle 来反序列化没有经过数据清洗的输入数据。同时,还推荐开发者通过实现自己的 Unpickler,来限制反序列化过程中能够 import 的类或函数,参考:https://docs.python.org/3.7/library/pickle.html#restricting-globals

0x01 RestrictedUnpickler

回到题目中。题目中 pickle “沙箱” 的大致逻辑如下:

可以看到,这个题是基于 Python 文档中的 example 来修改的,通过添加“黑名单”的方式来限制 unpickling 过程中的可导入类: 基本上禁用了 builtins 中的所有能直接执行代码的内置函数,同时还限制了导入类或者函数所在的模块必须是 builtins。可是,如果不通过这些内置函数,我们就不能执行代码了么?

翻翻文档,快速扫一眼 Python 的内置函数:

除去被禁用的函数,一眼看去还能凑合使用的是 mapfilter 这两个接受函数做参数的内置函数。

在尝试了多次之后发现,即使我们能传入函数参数,我们也只能执行 builtins 中仅有的几个函数,并不能执行任意代码。就在纠结万分的时候,我猛然发现,还有一个被忽略的点, 那就是上下文早已导入的 pickle 模块。我们是否可以想办法调用 pickle.loads呢?这样不就直接绕过了原有的“沙箱” ?

0x02 Pickle Protocol

查看一下 pickle 序列化协议,当前环境下使用的是 Python3,所以我们来看看当前的 Protocol 3:

输出 opcode 表:

对照此表,我们来看一个例子:

输出如下:

 

通过以上例子,我们使用 builtins 仅有的几个函数来构造一个间接调用 pickle.loads 的 Payload:

构造的 opcode 如下:

我们先构造一个内嵌的 payload:

 

结合两个 payload,我们来试一试:

可以看到,我们的 Payload 成功执行并读取了/etc/passwd。同时也可以看到,find_class 导入的函数全部满足条件:getattrlistfilterglobalsdict

0x03 小结

当前只能想到这样一种绕过,不知道是否还有其他的方式,只能坐等 dalao 们放 writeup了。虽然直接构造opcode难度不是很大,但是一个不小心也导致调试过程中好几次栈没有平衡。

以此也可以看出,在pickle反序列化过程中只禁用危险函数也是不够的,还是得谨慎使用该接口。

0x04 References

1. https://github.com/phith0n/code-breaking/blob/master/2018/picklecode/web/core/serializer.py
2. https://docs.python.org/3.7/library/pickle.html#restricting-globals

发表评论