
1. 项目概述当Pickle遇上WAF一场猫鼠游戏的开始如果你是一名Python开发者或者对Web安全稍有涉猎那你肯定听说过“Pickle反序列化漏洞”这个老生常谈却又屡试不爽的安全问题。简单来说Python的pickle模块就像是一个“魔法打包器”它能把内存里活生生的Python对象变成一串字节流方便存储或传输反过来它也能把这串字节流“复活”成原来的对象。这个“复活”的过程就是反序列化。问题就出在这个“复活”机制上——它太信任输入的数据了会忠实地执行序列化数据里包含的指令这其中就包括了调用一些危险的函数比如os.system、eval从而让攻击者有机会执行任意代码。正因为如此稍微有点安全意识的应用都会在接收Pickle数据的地方竖起一道“墙”——Web应用防火墙WAF。这道墙的核心策略往往是黑名单过滤把已知的危险函数名、模块名比如os、sys、eval、exec、__reduce__统统列入黑名单一旦在序列化数据里发现这些“敏感词”就立刻拦截请求。这听起来很完美对吧但安全攻防从来都是一场动态的猫鼠游戏。防守方筑起高墙攻击方就会寻找墙上的裂缝。今天我们要聊的就是当这道WAF黑名单墙看起来密不透风时作为攻击方或者说作为安全研究员可以尝试的五种“骚操作”它们的目标一致在不触发黑名单关键词检测的前提下构造出能够实现任意代码执行或敏感信息泄露的Pickle Payload。这篇文章不是教你去做坏事而是从一个防御者的角度深入理解攻击者的思维和手段。只有知道墙可能从哪些地方被突破你才能把墙修得更坚固。我们会从一道经典的CTF题目DASCTF 2024出发拆解其严格的过滤逻辑然后一步步展示五种不同思路的绕过技巧。这些技巧有的利用Python对象模型的特性有的借助文件操作“曲线救国”有的则玩起了指令集的“花活”。无论你是负责代码安全的开发者还是对Python底层机制充满好奇的学习者相信这篇近万字的实战剖析都能让你对Pickle反序列化有全新的、更深刻的认识。2. 靶场搭建与WAF规则深度解析在开始我们的“骚操作”之前必须先彻底了解我们要面对的是什么。纸上谈兵永远不如真枪实弹因此我强烈建议你在一个安全的隔离环境比如虚拟机或Docker容器中搭建一个模拟靶场。这里我们基于Flask快速构建一个存在Pickle反序列化漏洞的端点并模拟一个典型的、基于字符串匹配的WAF黑名单。2.1 模拟漏洞环境搭建首先我们创建一个名为vuln_app.py的Flask应用from flask import Flask, request, render_template_string import pickle import base64 import sys app Flask(__name__) # 一个简单的前端页面用于提交Payload HTML_FORM !DOCTYPE html html headtitlePickle Test/title/head body h2Pickle 反序列化测试接口/h2 form action/unpickle methodPOST textarea namedata rows10 cols80 placeholder请输入Base64编码的Pickle数据.../textareabr/br/ input typesubmit value提交 /form hr h3结果/h3 pre{{ result }}/pre /body /html app.route(/) def index(): return render_template_string(HTML_FORM, result等待提交...) app.route(/unpickle, methods[POST]) def unpickle(): data request.form.get(data, ) result # 模拟一个非常严格的黑名单WAF BLACKLIST { os, sys, subprocess, commands, popen, system, eval, exec, compile, input, __import__, open, file, __reduce__, __reduce_ex__, __setstate__, __getstate__, builtins, __builtins__, globals, locals, vars, getattr, setattr, delattr, apply, map, filter, \\, class, mro, bases, subclasses, import, from, as, pickle, marshal, shelve, cPickle } try: # 1. Base64解码 pickle_data base64.b64decode(data) raw_data_str pickle_data.decode(latin-1) # 转换为字符串进行关键词检查 # 2. 严格的WAF黑名单检查 for forbidden in BLACKLIST: if forbidden in raw_data_str: result fWAF拦截检测到黑名单关键词: {forbidden} return render_template_string(HTML_FORM, resultresult) # 3. 额外的危险模块“污染” # 模拟一些题目中常见的操作将危险模块替换掉使其在反序列化时不可用 sys.modules[os] None sys.modules[sys] None sys.modules[subprocess] None # 4. 执行反序列化 obj pickle.loads(pickle_data) result f反序列化成功对象类型: {type(obj)} if hasattr(obj, __repr__): result f\n对象内容: {repr(obj)} except Exception as e: result f反序列化失败或处理出错: {str(e)} return render_template_string(HTML_FORM, resultresult) if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)运行这个应用 (python vuln_app.py)访问http://127.0.0.1:5000你就得到了一个带有“严格”WAF的Pickle反序列化测试平台。2.2 WAF规则逻辑拆解与弱点分析我们的WAF模拟了现实中可能遇到的一种严格但并非无懈可击的防御策略。我们来逐条分析它的逻辑和潜在弱点关键词字符串匹配这是最核心的防御。它直接将序列化后的字节流解码成字符串这里用了latin-1编码以保证兼容性然后检查其中是否包含黑名单里的任何子字符串。这种方法简单粗暴但存在几个致命问题编码绕过Pickle协议数据是字节流直接解码为字符串进行检查可能会遗漏通过非标准编码或字节操作隐藏的关键词。例如将关键词拆分成多个部分或者利用协议指令本身的特性。指令集特性Pickle协议有自己的指令集如GLOBAL、INST、OBJ等。攻击者可以不直接出现函数名的字符串而是通过指令引用模块和函数这可能会绕过基于纯字符串的检查。大小写与变形虽然我们的黑名单是大小写敏感的但Python的模块和函数名本身是大小写敏感的所以这一点影响不大。但有些蹩脚的WAF可能只检查小写形式。模块污染在反序列化前将sys.modules中的危险模块如os、sys设置为None。这旨在阻止攻击者通过GLOBAL指令直接导入这些模块。这是一个有效的补充防御但它只影响了通过sys.modules的标准导入路径。攻击者仍然可能通过其他方式获取到这些模块或等价的函数。黑名单的“非完备性”这是所有黑名单机制的根本缺陷。你只能禁止你知道是危险的东西。Python的标准库和内置函数浩如烟海总有一些“不起眼”的函数或模块组合起来能产生意想不到的效果。我们的黑名单虽然长但远未覆盖所有可能性。例如io模块下的文件操作、types模块动态创建函数、甚至一些魔术方法__getattribute__的巧妙利用都可能成为突破口。实操心得在真实环境中WAF的规则可能更复杂可能结合正则表达式、语法树分析甚至机器学习模型。但基于字符串匹配的黑名单仍然非常常见尤其是在一些自定义的、轻量级的过滤逻辑中。理解这种防御的局限性是构思绕过方法的第一步。我们的五种“骚操作”正是针对这些弱点设计的。3. 绕过姿势一利用GLOBAL指令与builtins模块的“间接寻址”这是最经典也是理解后续所有高级技巧的基础。当os、sys等模块被直接屏蔽时我们如何获取到执行命令的能力答案是builtins。3.1 原理剖析builtins——Python的“武器库”builtins模块包含了Python的所有内置函数和异常比如print、len、open当然也包括__import__、eval、exec。最关键的是builtins模块在Python中具有特殊地位它是默认的命名空间很多函数可以直接调用而无需导入。在Pickle反序列化时我们可以通过GLOBAL指令来引用builtins模块下的函数即使os模块被污染了。GLOBAL指令的格式是cmodule\nname\n。例如cos\nsystem\n表示引用os.system。如果os模块被设置为None这条指令就会失败。但是cbuiltins\n__import__\n却可能成功因为它引用的是builtins.__import__。3.2 攻击链构建从__import__到os.system我们的目标是执行系统命令。思路如下通过builtins.__import__函数动态导入os模块注意这里导入的是新模块不受之前sys.modules[os] None的影响因为__import__会尝试重新加载。调用导入的os模块的system方法。手工构造这样的Pickle字节码非常繁琐且容易出错。因此安全社区开发了辅助工具最著名的就是pkerPickle Exploit Ker。我们可以用类Python的语法来描述攻击链然后由pker生成对应的Pickle字节码。假设我们没有pker我们也可以手动推导。但为了清晰我们先展示pker的思路。创建一个exploit.py# 这是一个概念性代码用于说明pker的生成逻辑并非直接运行 # 实际使用需要安装或编写pker工具 import pickle import base64 # 使用pker的“语言”描述攻击链伪代码 # GLOBAL(builtins, __import__) - 得到一个函数对象我们称它为 import_func # import_func(os) - 调用这个函数传入参数os得到os模块对象 # 从os模块对象中获取 system 属性 - 得到 os.system 函数 # 调用 os.system(id) - 执行命令 # 手工构造的简化版本不完整仅示意 # 这非常复杂涉及到栈操作PROTO, GLOBAL, TUPLE, REDUCE等 payload bcbuiltins __import__ p0 0(Sos tRp1 0g1 (Ssystem tRp2 0g2 (Sid tR. # 实际上上面的payload是不完整的正确的构造需要更精确的指令序列。由于手工构造极易出错我们直接给出一个利用pker假设我们有这个工具生成的有效Payload并分析其绕过原理cbuiltins getattr p0 0(cbuiltins __import__ Sos tRp1 0g0 (g1 Ssystem tRp2 0g2 (Sid tR.绕过分析整个Payload中没有出现os、sys、eval等被禁用的模块名作为字符串。os是作为参数Sos传递给__import__的而参数字符串可能不在WAF的检查范围内取决于WAF实现我们的模拟WAF会检查。关键函数__import__和getattr都来自builtins模块。builtins本身可能不在黑名单中或者即使builtins在名单里我们也可以尝试用其他方式获取这些内置函数比如通过().__class__.__bases__[0].__subclasses__()遍历但这种方法通常会产生包含class、bases等关键词的字符串容易被拦截。注意事项这种方法的有效性高度依赖于WAF的实现。如果WAF不仅检查模块/函数名还深度解析Pickle指令识别出GLOBAL指令后面的模块名builtins和函数名__import__、getattr那么它仍然可能被拦截。此外如果builtins也被加入黑名单此路不通。但现实中将builtins完全禁用会导致很多正常功能失效因此较少见。4. 绕过姿势二文件操作read/write实现非交互式信息泄露当直接执行命令的路径被彻底封死时例如黑名单包含了所有常见的内置危险函数和builtins我们就需要转变思路。目标从“执行代码”变为“获取信息”。在很多CTF题目或实际场景中最终目标是读取服务器上的一个特定文件如/flag、/etc/passwd。如果应用本身存在文件读取或回显的功能我们就可以利用它作为“通道”。4.1 场景复现与攻击链设计回顾我们引用的DASCTF 2024题目。它的WAF黑名单非常长但唯独没有过滤read和write。同时题目环境允许我们访问Web应用的源代码文件/app.py。这就为我们提供了完美的“曲线救国”方案攻击逻辑打开目标文件使用open(/flag)打开包含flag的文件。读取内容调用文件对象的read()方法将flag内容读入内存。写入可访问文件使用open(./app.py, w)以写入模式打开Web应用自身的源代码文件或任何其他前端可访问的文件。写入内容调用文件对象的write()方法将flag内容写入这个文件。前端回显通过正常的Web请求访问被篡改的app.py从而在前端看到flag内容。这个链条的核心是利用应用自身的文件读写和访问能力将敏感数据“搬运”到攻击者可以看见的地方。4.2 Payload构造与pker工具实战再次强调手工构造涉及文件操作的复杂Pickle Payload是噩梦。我们使用pker这里我们假设使用一个类似的工具或库来生成。pker允许我们用一种更直观的“伪Python”语法来描述操作# pker 语法示例 (概念性) getattr GLOBAL(builtins, getattr) open GLOBAL(builtins, open) # 1. 打开flag文件并读取 f_flag open(/flag) read_func getattr(f_flag, read) flag_content read_func() # 调用 read() # 2. 打开app.py文件并写入 f_app open(./app.py, w) write_func getattr(f_app, write) write_func(flag_content) # 调用 write() # 3. 返回None或其他无害对象 return Nonepker会将上面的逻辑编译成Pickle字节码。关键的绕过点在于open、read、write、getattr这些函数名本身可能不在黑名单中。即使open在黑名单里我们依然可以通过builtins.getattr(builtins, open)或者从其他对象中间接获取到它只要最终Payload字符串里不出现open即可。pker生成的字节码中函数名open是作为参数传递给GLOBAL指令的它可能以Sopen的形式存在。如果WAF检查所有字符串open就会被发现。因此更高级的绕过需要避免在Payload中出现任何敏感字符串。4.3 无字符串技术利用数字索引与属性访问如何避免出现open、read这样的字符串我们可以利用Python对象模型的特性。思路在Python中对象的属性可以通过getattr(obj, name)访问其中name是字符串。但属性也可以通过对象的__dict__或dir()列出然后通过索引访问。然而这又回到了需要知道属性名或遍历的问题。一个更巧妙的思路是利用builtins模块的__getitem__方法。builtins模块有一个__dict__属性它是一个字典包含了所有内置函数。我们可以通过数字索引来引用字典的值吗不行字典的键是字符串。但是我们可以先获取builtins.__dict__然后将其转换为list这样内置函数就变成了一个有序列表我们可以通过数字索引来获取它们。然而这又引入了list、__dict__等新字符串。这条路在严格的字符串过滤下也很难走通。因此对于基于字符串匹配的WAF最有效的绕过往往不是完全消除字符串而是使用WAF名单之外的、功能等价的字符串。例如如果WAF禁了open但没禁file在Python 2中file是内置函数Python 3中它在io模块或许可以尝试。或者寻找其他具有读写能力的对象和方法。实操心得文件操作绕过的精髓在于“信息转移”。它不追求直接RCE远程代码执行而是追求数据泄露。在实际渗透测试中这种思路非常实用。例如如果你能写入Web目录下的一个文件比如/tmp/shell.php并使其可执行那么结合文件包含漏洞就可能实现RCE。或者将敏感信息写入日志文件然后通过正常的日志查看功能读取。这种“分步走”、“组合拳”的策略往往能突破单点防御。5. 绕过姿势三魔术方法__setstate__与__getstate__的妙用Python的Pickle协议在序列化和反序列化对象时会调用一些特殊的魔术方法Magic Methods。除了众所周知的__reduce____setstate__和__getstate__也扮演着重要角色并且常常被WAF忽略。5.1__setstate__的工作原理当一个对象被反序列化unpickle时如果它的类定义了__setstate__方法Pickle会调用这个方法并将序列化时__getstate__返回的状态或者默认的__dict__作为参数传递给它。这给了我们一个在反序列化过程中自动执行代码的机会。import pickle class EvilClass: def __setstate__(self, state): import os os.system(echo Code executed via __setstate__!) # 序列化 payload pickle.dumps(EvilClass()) # 反序列化时__setstate__会被自动调用 pickle.loads(payload)绕过优势隐蔽性Payload中可能只包含类名EvilClass而真正的恶意代码藏在类定义里。如果WAF只检查序列化数据流中的字符串它可能发现不了os.system因为这个调用发生在类的方法内部而方法体本身并不直接存在于序列化的字节流中序列化保存的是对类EvilClass的引用而不是其源代码。然而如果EvilClass是动态定义的并且其类名或模块路径中包含敏感词还是可能被拦截。灵活性可以在__setstate__里做任何事包括调用被禁用的模块和函数因为此时代码执行环境已经跳出了WAF的字符串过滤阶段。5.2 如何将恶意类“送”进去这里有一个关键问题我们的恶意类EvilClass必须存在于反序列化环境的命名空间中。在CTF题目中出题人有时会“贴心”地给出一个自定义的、存在漏洞的类。但在更普遍的情况下我们需要利用Python的对象继承链来动态构造或访问一个已有的、可控的类。一个常见技巧是利用Python中广泛存在的、可序列化的标准库类并尝试覆盖其__setstate__方法。但这通常需要利用到__reduce__来返回一个元组指定一个可调用对象和参数来重建对象而这又回到了__reduce__被禁的问题。更可行的方式是如果环境中存在任何用户可控的、可被序列化的对象并且我们能控制其__class__属性这很难或者能找到一个其__setstate__方法行为可疑的现有类。例如一些标准库类如pickle.Unpickler、socket._socketobject等的__setstate__可能涉及资源分配或状态恢复如果我们能构造特定的state参数或许能触发非预期的行为。但这需要深入的知识和具体的环境分析。因此__setstate__绕过更像是一种“机会主义”的技巧。当题目源码中已经定义了一个类并且这个类会被序列化/反序列化而它的__setstate__方法实现有缺陷或我们可以控制传入的state时这个方法就非常强大。如果我们需要从零开始注入一个全新的恶意类则通常需要配合其他能执行代码的 primitive如exec或eval来动态创建这个类而这又可能被WAF拦截。注意事项使用__setstate__时要确保类本身可以被安全地序列化和反序列化。另外__setstate__接收的state参数通常是字典如果我们可以控制这个字典的内容也许能进行属性注入攻击但这属于另一个攻击面利用__dict__.update(state)。6. 绕过姿势四操作码拼接与指令流混淆前几种方法主要是在“做什么”的层面进行绕过。而操作码混淆则是在“怎么做”的层面进行干扰。Pickle协议本质上是一种基于栈的虚拟机指令集。我们可以直接操作这些底层指令操作码来构造WAF难以识别的Payload。6.1 Pickle指令集简介Pickle协议定义了一系列单字节的操作码opcode例如c: GLOBAL推入一个全局对象模块.名称(: MARK在栈上标记一个位置S: STRING推入一个字符串t: TUPLE从栈顶弹出多个元素组成元组R: REDUCE应用可调用对象p: PUT将栈顶对象存储到memo字典g: GET从memo字典获取对象.: STOP停止我们之前看到的文本格式如cbuiltins\ngetattr\n是协议0文本协议的表示形式。更高版本的协议如协议2, 3, 4, 5使用二进制格式可读性更差但原理相同。6.2 混淆技术实战WAF的字符串匹配通常针对的是协议0的文本表示或解码后的字符串。我们可以通过以下方式进行混淆使用高版本协议使用pickle.dumps(obj, protocol4)生成二进制Payload。二进制数据中操作码和参数混杂简单的字符串匹配很难准确提取出builtins、system这样的关键词除非WAF也实现了完整的Pickle解析器。直接将二进制数据Base64后提交可能绕过仅检查文本格式的WAF。指令拆分与重组利用PUT(p)和GET(g)指令。我们可以将一个长字符串或复杂对象拆分成多个部分分别存储到memo中然后组合使用。例如不直接使用字符串os.system而是先分别存储os和system再组合。这可能会打乱字符串在字节流中的连续出现。# 未混淆 Sos.system # 混淆后 (概念性) Sos p0 # 存储到memo[0] Ssystem p1 # 存储到memo[1] g0 # 取出 os g1 # 取出 system # 如何组合成 os.system 需要其他指令这里只是示意拆分思路实际上组合需要用到BUILD或REDUCE等指令会引入更多操作码使得Payload更复杂字符串特征更分散。利用注释和空白在协议0中#之后的内容是注释会被解析器忽略。我们可以在Payload中插入大量无意义的注释和换行干扰字符串匹配。cbuiltins # 这是一段无害的注释os.system什么的都不在这里 getattr p0 0 # 更多注释...如果WAF是简单的逐行或全文字符串搜索这些注释没有影响。但如果WAF尝试先去除注释再检查那可能无效。不过很多简单的WAF不会做这么复杂的预处理。Unicode与编码技巧将关键字符串用特殊的Unicode字符如零宽字符包裹或者使用不同的编码如UTF-16进行表示然后确保在Pickle解析时它能被正确解码。这要求对Pickle的字符串解析机制有很深的理解且成功率不高因为Pickle协议对字符串的编码有明确规定通常是ASCII或Latin-1相关的。这种方法的局限性现代WAF和专业的反序列化过滤器如pickle.Unpickler的子类重写find_class方法是在解析器层面进行拦截的。它们会真正解析Pickle字节码在GLOBAL、INST等指令被解析时检查即将导入的模块和类名。在这种情况下无论你怎么混淆操作码流最终解析出来的模块/函数名都会暴露。因此操作码混淆主要对抗的是基于字节流或字符串正则匹配的浅层WAF对于实现了完整解析和钩子函数的安全模块效果有限。排查技巧如果你怀疑WAF是基于字符串匹配的可以尝试发送一个包含大量随机注释和空白的、无害的Payload比如只序列化一个数字1观察是否被拦截。如果被拦截说明WAF可能在做简单的全文匹配混淆可能有效。如果无害Payload能通过但包含builtins的Payload被拦说明WAF很可能在解析后检查。7. 绕过姿势五利用types、functools等“白名单”模块进行函数构造当直接的危险函数和builtins都被封禁时我们的视野需要扩大到整个Python标准库。有一些模块本身看似无害但可以用来构造出具有执行代码能力的函数。types和functools就是这样的“瑞士军刀”。7.1 使用types.FunctionType动态创建函数types.FunctionType可以用于创建一个新的函数对象。你需要提供代码对象code、全局命名空间globals和一个函数名。如果我们能构造出一个执行任意代码的代码对象就能创建一个函数并调用它。如何获取代码对象可以通过compile内置函数。但compile很可能在黑名单里。另一种方式是寻找一个现有的、简单的代码对象然后修改它的字节码这涉及到底层CPython的code对象结构极其复杂且不稳定。一个更现实的路径是如果我们能执行任意字符串我们就能用exec或eval。但这两个也被禁了。那么有没有其他方式“模拟”出exec的功能可以考虑types.ModuleType动态创建模块然后向模块的命名空间里注入代码字符串再import它这又回到了需要exec或__import__。这条路在完全禁用builtins和exec/eval的环境下非常艰难。它更依赖于环境中已经存在一些我们可控的、能够执行代码的“种子”。7.2 使用functools.partial进行函数柯里化functools.partial用于“部分应用”一个函数即固定函数的一些参数产生一个新的可调用对象。这个本身不能执行代码但它可以用于绕过对特定参数形式的检查。例如假设WAF愚蠢到只检查os.system(‘id’)这样的完整调用字符串。我们可以用partial先绑定函数再传递参数import functools import os cmd_executor functools.partial(os.system, id) # 现在 cmd_executor 是一个可调用对象调用它就会执行 os.system(id)在Pickle中我们可以序列化这个cmd_executor对象。Payload里包含的是functools.partial和os.system的引用以及参数id。如果WAF只检查os.system(‘id’)这个整体模式它可能会漏掉这种形式。但这是一种非常脆弱的绕过实战价值低。7.3 寻找“替代函数”核心思路是寻找那些不在黑名单上但功能上可以替代危险函数的“白名单”函数。os.popenvsos.system: 如果只禁了systempopen可能还能用它也能执行命令并返回输出。subprocess.call/run/Popen:os模块被禁但subprocess模块可能还在。importlib.import_module: 如果__import__被禁这个函数可能是一个替代品。getattr,setattr,delattr: 这些是属性访问的底层函数极其强大常被忽略。我们已经在姿势一中用到了getattr。breakpoint(): 在Python 3.7中这个内置函数会触发调试器。如果环境允许交互比如在某些CTF的pwn题中这可能是一个入口。codecs模块: 可以用于编码解码有时能配合其他漏洞进行数据泄露。这本质上是一种“白名单探测”。需要你对Python标准库非常熟悉并且能快速测试哪些模块和函数是可用的。在CTF中出题人有时会故意留一些这样的“后门”函数。实战心得这种绕过方式没有固定套路考验的是攻击者的知识广度和对环境的快速适应能力。在真实的漏洞利用中如果遇到严格过滤我会习惯性地在交互式Shell里如果能有的话快速测试dir(__builtins__)看看哪些内置函数还在然后尝试导入一些常见的非危险模块json,re,datetime,itertools等再深入这些模块的dir()寻找任何可能用于读写文件、执行代码或导入模块的函数。这个过程就像在迷宫里摸索需要耐心和创造力。8. 防御者视角从黑名单到纵深防御分析了这么多攻击手法作为开发者或安全工程师我们该如何防御单纯依赖黑名单是注定失败的因为攻击面太广。我们需要建立一套纵深防御体系。8.1 根本解决方案弃用Pickle最有效、最根本的防御就是不要使用pickle来反序列化不可信数据。对于大多数Web应用json、yaml需注意安全加载、msgpack等格式是更安全的选择。它们只序列化数据不序列化代码逻辑。# 安全替代 import json data json.dumps(user_object) # 仅序列化数据 user_obj json.loads(data) # 安全反序列化不会执行代码如果因为历史原因或特定需求如分布式计算框架必须使用Pickle请继续往下看。8.2 增强过滤使用Unpickler与find_class白名单不要自己写字符串过滤。使用pickle.Unpickler类并重写其find_class方法。这个方法在每次GLOBAL或INST指令试图加载一个模块/类时被调用。在这里实施白名单策略。import pickle import io class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): # 只允许反序列化来自安全模块的安全类 ALLOWED { (__main__, SafeClass), # 只允许应用内定义的特定类 (numpy, ndarray), # 如果确实需要可以按需添加 (pandas, DataFrame), } if (module, name) in ALLOWED: return super().find_class(module, name) # 拒绝所有其他访问 raise pickle.UnpicklingError(fGlobal {module}.{name} is forbidden) def safe_loads(data): 安全地反序列化Pickle数据 return RestrictedUnpickler(io.BytesIO(data)).load() # 使用 try: obj safe_loads(untrusted_data) except pickle.UnpicklingError as e: print(f安全拦截: {e})这是业界推荐的最佳实践。白名单比黑名单安全得多。你需要仔细评估你的应用真正需要反序列化哪些类并将它们明确列入白名单。8.3 环境隔离沙箱与低权限运行即使采用了白名单也不能保证100%安全白名单内的类也可能存在漏洞。因此需要额外的隔离层进程隔离在一个独立的、低权限的进程或容器中执行反序列化操作。这个进程应该被严格限制例如使用seccomp、AppArmor、SELinux限制系统调用使用chroot限制文件系统访问使用resource模块限制CPU/内存。系统权限运行Web服务的用户应该是一个非特权用户如www-data,nobody没有sudo权限对系统关键目录只有读权限。网络隔离反序列化环境不应该有出网权限防止反弹Shell。8.4 运行时监控与审计日志记录详细记录所有反序列化操作包括来源IP、时间、试图加载的模块/类。对异常模式如频繁尝试加载非常见类进行告警。行为监控监控反序列化进程的系统调用特别是execve执行新程序、open打开文件、connect网络连接等。可以使用auditd、Falco等工具。文件系统监控对Web目录、临时目录、配置文件等敏感位置设置文件完整性监控如aide,tripwire防止被篡改。8.5 代码审计与依赖管理定期审计使用静态代码分析工具如Bandit,Semgrep扫描代码库查找不安全的pickle.loads调用。依赖检查确保使用的第三方库没有不安全的反序列化点。关注安全公告。最小权限原则即使是白名单里的类也要确保其实现是安全的没有危险的副作用或可被利用的魔术方法。防御Pickle反序列化漏洞是一场持久战。没有银弹必须结合安全编码、严格过滤、环境隔离和持续监控才能构建起有效的防线。而作为攻击方安全研究员所研究的这些绕过技巧其最大价值正是帮助防御者看清防线的薄弱之处从而将其加固。