不安全的反序列化

2015年,不安Apache Commons Collections库的反序一个反序列化漏洞让WebLogic、JBoss、列化WebSphere等一堆Java中间件集体沦陷。不安攻击者只需要发一段精心构造的反序序列化数据,就能在服务器上执行任意命令。列化这不是不安某个冷门组件的边角问题,是反序核心基础库出了事。

反序列化的列化本质是什么?程序把对象状态转成字节流存起来或传出去,需要时再还原回来。不安问题就出在“还原”这一步——数据从外部来,反序程序却当它是列化自己人。

为什么这事容易出问题

拿PHP来说。不安serialize()把对象变成字符串,反序unserialize()再变回来。列化看起来对称、干净。但PHP的unserialize()在还原对象时,会自动调用对象的__wakeup()或__destruct()魔术方法。如果攻击者能控制序列化字符串的内容,就能控制哪些类被实例化、属性被设成什么值。再配合目标项目里已有的类,把魔术方法当跳板,一路串联到危险操作上——写文件、执行命令、连数据库,都有可能。

Java那边更夸张。ObjectInputStream.readObject()在还原时会触发readObject()方法,而大量第三方库——Commons Collections、Spring、Groovy——都有可以被利用的readObject链。ysoserial这个工具把常见利用链打包好了,选个gadget,填个命令,生成payload,完事。

Python的pickle同理。pickle.loads()能执行任意代码,官方文档自己都写了“永远不要反序列化不可信的数据”。但写代码的人看文档吗?不一定。

真实的攻击面比想象中大

很多人觉得反序列化漏洞离自己很远。实际场景里,它藏在很多不起眼的地方:

HTTP请求里的Cookie值。有些框架把用户会话序列化后塞进Cookie,服务端取出来直接反序列化。攻击者改Cookie就行。

RMI调用。Java的远程方法调用默认走序列化,如果RMI端口暴露,基本等于敞开大门。

缓存系统。把对象序列化后存Redis或Memcached,取出来反序列化。如果缓存能被写入……链路就通了。

消息队列。RabbitMQ、Kafka里的消息体如果是序列化对象,消费者反序列化时同样中招。

我见过一个案例:某内部系统用Java序列化做RPC通信,觉得“内网嘛,安全”。结果一台边缘机器被拿下后,攻击者顺着RPC链路横向移动,最后拿到核心数据库。内网信任假设在反序列化面前一文不值。

防御的核心思路

最直接的办法:不反序列化不可信数据。用JSON、Protocol Buffers这类纯数据格式替代。JSON没有对象还原的概念,解析出来就是Map和List,不会触发魔术方法或readObject。

如果业务上非用不可,加白名单。Java可以用ObjectInputFilter限制允许反序列化的类,PHP可以用allowed_classes参数限定范围。但白名单维护起来烦,类多了容易漏,这是现实。

还有一种做法是签名。序列化数据发出前加HMAC,接收方先验签再反序列化。这能挡住篡改,但挡不住密钥泄露的情况。

说到底,反序列化漏洞的根因是设计层面的:把数据当代码执行了。只要这个机制存在,就永远有人踩坑。作为开发者,看到unserialize、readObject、pickle.loads这些调用,条件反射地警觉一下,不丢人。