游客发表
PHP的serialize()和unserialize()这对函数,本质上就是化漏对象和字符串之间的翻译官。序列化把对象拍扁成一段可存储的洞原字符串,反序列化再把字符串捏回对象。序列问题出在“捏”这个动作上——如果字符串被攻击者动过手脚,化漏反序列化出来的洞原对象就可能变成一颗遥控炸弹。
先看一个最朴素的序列类:
class User { public $name = 'guest'; public $isAdmin = false; public function __destruct() { if ($this->isAdmin) { // 执行管理员操作,比如删除日志 echo "Admin action!"; } } } 正常序列化new User()得到:O:4:"User":2:{ s:4:"name";s:5:"guest";s:7:"isAdmin";b:0;}。化漏这段字符串如果被攻击者拿到,洞原他只需把isAdmin改成b:1(布尔真),序列再传回给unserialize()。化漏对象重建时$isAdmin就成了true,洞原__destruct()一触发,序列越权操作就发生了。化漏这就是洞原反序列化漏洞的核——用户可控的数据进入了unserialize(),而类里恰好有“魔术方法”在特定时机自动执行。
魔术方法才是真正的导火索。__wakeup()在反序列化后立刻调用,__destruct()在对象销毁时调用,__toString()在对象被当字符串用时调用。攻击者不需要直接调用这些方法,他只要构造一个恶意序列化字符串,PHP引擎自己就会在合适的时机把代码送进这些方法里。比如下面这个经典场景:
class FileReader { public $filename; public function __wakeup() { echo file_get_contents($this->filename); } } 攻击者构造O:10:"FileReader":1:{ s:8:"filename";s:11:"/etc/passwd";},反序列化时__wakeup()自动执行,服务器上的敏感文件就被读取并输出。整个过程不需要任何显式的函数调用,漏洞利用链完全由PHP的魔术方法触发。
CVE-2015-8562,攻击者通过HTTP头注入序列化数据,利用__destruct()链最终实现远程代码执行。当时大量站点中招,原因就是用户输入未经严格过滤就进入了反序列化流程。 更麻烦的是POP链(Property-Oriented Programming)。单个类的魔术方法可能只做一件小事,比如__toString()返回一个属性。但攻击者可以把多个类串起来:A类的__destruct()调用B类的某个方法,B类的方法又触发C类的__call(),最终在某个类里找到eval()或system()。整条链像多米诺骨牌,PHP引擎按部就班地推倒每一张,攻击者只需精心摆放第一张牌——那个恶意序列化字符串。
防御这件事,说复杂也复杂,说简单就两条线。第一,永远不要反序列化用户可控的数据。如果业务必须传对象,用JSON,它没有魔术方法,不会自动执行代码。第二,如果实在避不开unserialize(),给类加上__wakeup()或__unserialize()做完整性校验,比如HMAC签名。PHP 7.4之后还可以用allowed_classes参数白名单限制能反序列化的类,这招能直接掐死大部分POP链。
写PHP代码时看到unserialize(),心里就该拉响警报。它不像eval()那么臭名昭著,但危险程度一点不低。魔术方法本身是好设计,让对象有了生命周期钩子,可一旦对象的内容能被外部操控,这些钩子就成了攻击者的提线木偶。
顺带提一句,PHP 8.0对反序列化做了一些安全改进,比如__unserialize()的引入让开发者可以更细粒度地控制重建过程。但工具再好,也架不住往unserialize()里直接塞$_GET['data']。漏洞的根因从来不在语言,而在使用语言的人。
友情链接