Python Pickle 反序列化漏洞

pickle 是 Python 中用于序列化和反序列化对象的模块。序列化是将对象转换为字节流的过程,反序列化是将字节流还原为对象的过程。它常被用来:

  • 存储 Python 对象(变量、列表、字典、类实例等)
  • 用来存储模型参数(但不推荐!LLM 现在更常用 safetensors

import pickle
import os
import torch.nn as nn


class Demo(nn.Module):
    def __init__(self):
        super(Demo, self).__init__()
        self.linear1 = nn.Linear(128, 256)
        self.linear2 = nn.Linear(128, 256)

def test():
    pickle.dump(Demo(), open('demo.bin', 'wb'))
    demo = pickle.load(open('demo.bin', 'rb'))


if __name__ == '__main__':
    test()

但是,使用 Pickle 存在是一个严重的安全风险。在反序列化时,Pickle 可以执行存储在对象中的任意 Python 代码。 这意味着攻击者可以构造恶意的 .pkl.bin 文件,如果你的模型文件是从陌生网站来源下载的,攻击者可以植入恶意代码。

1. 漏洞描述

在 Python 中,pickle 模块用于序列化和反序列化对象,通常使用 pickle.dumppickle.load 进行操作。然而,这个过程中存在一个安全隐患,主要源自 __reduce__ 魔术方法。

接下来,先了解下__reduce__ 魔术方法的作用,它主要是用于在反序列化恢复对象的时候调用(默认不需要手动添加,只有当特殊情况下才会手动编写)。

import pickle

class Demo:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __reduce__(self):
        return (self.__class__, (self.a, self.b))

def test():
    # 将 __class__ 对应的类名及其所在模块路径存储到文件中
    # 将 self.a 和 self.b 对应的值存储到文件中
    pickle.dump(Demo(10, 20), open('demo.bin', 'wb'))
    # 从文件中加载数据,并根据存储的模块路径找到名字为 Demo 的类
    # 使用该类和存储的参数值创建对象
    demo = pickle.load(open('demo.bin', 'rb'))

if __name__ == '__main__':
    test()

当一个类实现了 __reduce__ 魔术方法时,pickle 在序列化该对象时会调用 __reduce__,该方法应返回一个元组,其中第一个元素是一个可调用对象,第二个元素是传递给该对象的参数元组。在反序列化时,pickle 会调用这个可调用对象,并传入提供的参数来重建对象。

那么,如果别有用心者在 __reduce__ 返回值上做手脚,即:返回一个具有恶意行为的可调用对象。一旦不知情的用户加载这个 pickle 文件,恶意代码便会执行,从而造成安全威胁。

2. 场景复现

再次明确:Pickle 的安全漏洞主要是由于别有用心者滥用了 __reduce__ 函数,并将返回的正常的可调用对象替换为具有恶意行为的可调用对象。

import pickle
import os
import torch.nn as nn


# 恶意代码的字符串表示
malicious_code_str = """
def malicious_code():
    print("no safe code!")

malicious_code()
"""


class Demo:
    def __reduce__(self):
        return (exec, (malicious_code_str,))
        # return (os.system, ('echo no safe command!',))


def test():
    demo = Demo()
    # 序列化包含恶意程序的对象
    with open('no-safe.pkl', 'wb') as f:
        pickle.dump(demo, f)


if __name__ == '__main__':
    test()

这段代码已经生成了 no-safe.pkl 文件,接下来反序列化该文件:

import pickle

def test():
    demo = pickle.load(open('no-safe.pkl', 'rb'))

if __name__ == '__main__':
    test()
no safe code!

最后总结下,pickle 是一个强大的序列化模块,但在反序列化时存在安全风险。始终确保反序列化的数据来源可靠,避免反序列化用户输入的数据。如果安全性是关键需求,建议使用更安全的序列化格式(如 json、safetensors)。如果必须使用 pickle,可以通过自定义反序列化逻辑来限制可反序列化的类。

import pickle

# 恶意代码的字符串表示
malicious_code_str = """
def malicious_code():
    print("no safe code!")

malicious_code()
"""

class Demo:
    def __reduce__(self):
        return (exec, (malicious_code_str,))
        # return (self.__class__, ())

# 自定义反序列化类
class SafeUnpickler(pickle.Unpickler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 定义允许反序列化的类
        self.allowed_classes = {'__main__.Demo': Demo}

    # 重写 find_class 函数,该函数在反序列化时被调用,用于根据模块名和类名找到对应的类
    def find_class(self, module, name):
        # module 和 name 是从序列化文件中读取的模块和类型
        # 检查模块和类名是否允许反序列化该类
        allowed_class = f'{module}.{name}'
        if allowed_class not in self.allowed_classes:
            raise pickle.UnpicklingError(f'尝试去反序列化不支持的类型 {allowed_class}')
        return self.allowed_classes[allowed_class]


def test():
    demo = Demo()
    pickle.dump(demo, open('no-safe.pkl', 'wb'))
    # 使用自定义反序列化
    with open('no-safe.pkl', 'rb') as f:
        demo = SafeUnpickler(f).load()
    print(demo)

if __name__ == '__main__':
    test()

未经允许不得转载:一亩三分地 » Python Pickle 反序列化漏洞
评论 (0)

8 + 1 =