CVE-2018-12613漏洞复现:phpMyAdmin 4.8.1文件包含与白名单绕过实战

CVE-2018-12613漏洞复现:phpMyAdmin 4.8.1文件包含与白名单绕过实战
1. 项目概述与核心价值最近在整理一些老漏洞的复现笔记发现phpMyAdmin 4.8.1这个版本的文件包含漏洞CVE-2018-12613虽然过去几年了但作为Web安全学习中的一个经典案例其原理和利用思路依然非常有嚼头。很多新手朋友在学习代码审计或漏洞复现时往往卡在环境搭建和利用链的构造上感觉看懂了原理却无从下手。这篇指南我就从一个一线安全从业者的角度带你完整地走一遍这个漏洞的复现流程。我们不仅要把漏洞“跑通”更要深挖一下它为什么会出现以及在实际的渗透测试或代码审计中遇到类似的白名单绕过、路径拼接问题应该如何去思考和定位。简单来说这个漏洞允许经过身份验证的攻击者通过精心构造的请求参数在服务器上包含并执行任意本地文件。它不是一个简单的直接文件包含而是涉及到了phpMyAdmin对index.php中target参数的处理逻辑缺陷核心在于对“白名单”校验的绕过。对于想入门代码审计的朋友这是一个绝佳的练手材料对于已经有一定基础的朋友重温这个漏洞也能帮助我们反思在代码设计时对用户输入过滤和路径处理的常见误区。2. 漏洞原理深度剖析2.1 漏洞触发点与代码逻辑漏洞的核心位于phpMyAdmin 4.8.1版本以及之前的部分版本的index.php文件中。当我们访问phpMyAdmin时通常会有一个target参数用来指定要加载的页面或模块。开发者的初衷是好的他们设置了一个白名单只允许加载libraries目录下的一些特定文件比如import.php,export.php等以防止目录穿越攻击。问题出在代码的实现上。我们来看一下关键的代码片段经过简化和注释以便理解// index.php 中的部分代码 if (! empty($_REQUEST[target]) is_string($_REQUEST[target]) ! preg_match(/^index/, $_REQUEST[target]) ! in_array($_REQUEST[target], $target_blacklist) Core::checkPageValidity($_REQUEST[target]) ) { include $_REQUEST[target]; exit; }表面上看这里有多重检查必须是字符串、不能以index开头、不能在黑名单里最后还调用了Core::checkPageValidity函数进行白名单校验。问题就藏在这个Core::checkPageValidity函数里。2.2 关键函数Core::checkPageValidity的缺陷让我们深入这个校验函数位于libraries/classes/Core.phppublic static function checkPageValidity($page, array $whitelist []) { if (empty($whitelist)) { $whitelist self::$goto_whitelist; // 从预定义的白名单数组加载 } if (! isset($page) || !is_string($page)) { return false; } // 关键点1使用strpos查找问号? if (strpos($page, ?) ! false) { $page substr($page, 0, strpos($page, ?)); } // 关键点2白名单校验 return in_array($page, $whitelist); }这个函数的逻辑是如果$page即target参数包含问号?则只截取问号之前的部分进行白名单校验。校验时只判断截取后的部分是否在白名单数组中。漏洞的根源就在这里校验和包含是分离的。函数只校验了问号前的内容但最终include语句包含的是原始的、未经截取的$_REQUEST[target]。2.3 构造攻击Payload理解了上述逻辑攻击思路就清晰了我们提供一个target参数其值在问号?之前的部分是白名单内的合法文件问号之后的部分则是我们想包含的恶意路径。例如白名单中包含db_sql.php。我们可以构造如下Payloadtargetdb_sql.php?/../../../../etc/passwdCore::checkPageValidity函数接收到targetdb_sql.php?/../../../../etc/passwd。它发现字符串中有问号于是截取问号之前的部分得到db_sql.php。检查db_sql.php是否在白名单中——是的通过校验函数返回true。回到index.php代码执行include $_REQUEST[target];此时包含的完整路径是db_sql.php?/../../../../etc/passwd。在PHP中include或require包含文件时如果路径中包含问号?PHP会将其解释为路径的一部分而不是查询字符串。更具体地说在默认的配置下PHP会尝试访问一个字面意思为db_sql.php?/../../../../etc/passwd的文件这显然不存在。但是在Unix-like系统上?是允许出现在文件名中的。当PHP找不到这个带问号的文件时在某些环境下它会将问号后的内容也作为路径的一部分进行解析从而可能实现目录穿越。另一种更可靠的利用方式是结合PHP的封装协议但这需要进一步的条件。注意这里有一个非常重要的细节。直接使用?进行路径穿越的成功率依赖于PHP和Web服务器的具体配置如cgi.fix_pathinfo的设置。在实际利用中更常见和可靠的方法是使用?的URL编码形式%3f或者利用#锚点及其编码%23。因为#在URL中表示片段标识符通常不会发送到服务器端但在构造本地文件路径时它可能被PHP解释器以不同的方式处理。例如targetdb_sql.php%253f/../../../../etc/passwd这里涉及双重编码或targetdb_sql.php%23/../../../../etc/passwd在某些场景下能更稳定地绕过校验并让PHP正确解析到后面的路径。这也是为什么你在不同复现文章里看到的Payload可能略有差异的原因它们都在尝试适应不同的环境。3. 靶场环境搭建实操纸上得来终觉浅绝知此事要躬行。下面我们一步步搭建一个可复现的漏洞环境。3.1 环境与工具准备我推荐使用Docker来搭建环境这是最干净、最便捷的方式可以避免污染本地系统也方便随时重置。操作系统不限能运行Docker即可Linux, macOS, Windows WSL2。Docker Docker Compose请确保已安装。这是我们的核心工具。代码编辑器如VSCode、Sublime Text用于查看和修改代码。浏览器任何现代浏览器用于访问phpMyAdmin。HTTP代理工具Burp Suite Community版或OWASP ZAP。用于拦截和修改HTTP请求这对于手动构造和发送攻击Payload至关重要。如果你习惯命令行curl也可以。3.2 使用Docker-Compose一键部署创建一个名为docker-compose.yml的文件内容如下version: 3.8 services: vulnerable-pma: image: phpmyadmin/phpmyadmin:4.8.1 container_name: pma-cve-2018-12613 ports: - 8080:80 environment: - PMA_HOSTdb - PMA_PORT3306 - UPLOAD_LIMIT100M depends_on: - db networks: - pma-network db: image: mysql:5.7 container_name: mysql-for-pma restart: always environment: MYSQL_ROOT_PASSWORD: rootpassword123 MYSQL_DATABASE: testdb MYSQL_USER: testuser MYSQL_PASSWORD: testpass123 volumes: - mysql-data:/var/lib/mysql networks: - pma-network networks: pma-network: driver: bridge volumes: mysql-data:配置解读vulnerable-pma服务使用官方phpmyadmin:4.8.1镜像将容器80端口映射到宿主机的8080端口。通过环境变量指定要连接的数据库主机为db即下面定义的MySQL服务。db服务使用mysql:5.7镜像设置root密码并预先创建一个测试数据库和用户。数据被持久化到名为mysql-data的卷中防止容器删除后数据丢失。两个服务通过自定义的pma-network连接确保phpMyAdmin容器能通过服务名db访问MySQL。在包含docker-compose.yml文件的目录下打开终端执行docker-compose up -d等待镜像拉取和容器启动。完成后在浏览器中访问http://localhost:8080你应该能看到phpMyAdmin 4.8.1的登录界面。3.3 登录与基础配置使用我们在docker-compose.yml中配置的数据库用户信息登录用户名testuser密码testpass123服务器保持默认或者选择db登录成功后你会进入熟悉的phpMyAdmin管理界面。至此一个包含漏洞的phpMyAdmin 4.8.1环境就准备就绪了。实操心得为什么选择Docker首先版本精准控制确保我们复现的就是4.8.1。其次环境隔离复现完成后一句docker-compose down -v就能彻底清理不留任何垃圾文件。最后这个docker-compose.yml文件你可以保存下来以后任何时候想重温这个漏洞一分钟就能把环境拉起来非常高效。4. 漏洞利用过程全解析环境有了现在我们开始“攻击”自己搭建的靶场。4.1 信息收集与入口点确认首先我们需要确认漏洞存在的入口点。根据原理分析漏洞存在于index.php对target参数的处理中。因此任何能访问到index.php的经过认证的会话都可能成为利用点。通常我们登录后的主界面就是由index.php渲染的。查看浏览器地址栏URL可能类似于http://localhost:8080/index.php。这就是我们的起点。4.2 手动构造与发送攻击请求我们不会在浏览器地址栏直接输入Payload那样不方便且容易出错。我们将使用Burp Suite来拦截和修改请求。配置浏览器代理将浏览器的HTTP代理设置为Burp Suite默认127.0.0.1:8080并安装Burp的CA证书首次使用需要。拦截请求在phpMyAdmin界面进行任何一次操作比如点击一个导航标签让Burp Suite拦截到发送给index.php的请求。修改请求在Burp的Proxy - Intercept标签页找到被拦截的GET或POST请求。你会看到请求行或请求体中可能有target参数或者根本没有。如果没有我们需要手动添加。 通常我们可以修改一个已有的请求。例如拦截一个类似GET /index.php?route/server/privileges... HTTP/1.1的请求。 我们将整个查询字符串替换为我们的Payload。注意为了确保Payload被正确发送我们需要对特殊字符进行URL编码。构造Payload的关键 回顾原理我们需要一个白名单内的文件作为“前缀”。通过查看phpMyAdmin 4.8.1的源码或网络资料常见的白名单文件包括import.php,export.php,db_operations.php,db_sql.php,server_privileges.php等。我们选择db_sql.php。一个经典的测试Payload是尝试包含/etc/passwd文件Linux系统。构造如下targetdb_sql.php%253f/../../../../etc/passwd这里使用了%253f它是问号?的双重URL编码?-%3f-%253f。这种编码有时能更好地绕过某些过滤或校验逻辑。发送请求在Burp的拦截界面将请求行修改为GET /index.php?targetdb_sql.php%253f/../../../../etc/passwd HTTP/1.1然后点击“Forward”发送这个修改后的请求。4.3 结果验证与解读发送请求后观察Burp Suite的“Response”标签页或者切换到浏览器查看。成功利用的迹象 如果漏洞存在且利用成功你可能会在HTTP响应中看到/etc/passwd文件的内容。响应体可能以phpMyAdmin的HTML框架开始但在页面某处可能是顶部、底部或中间会直接输出/etc/passwd的文本内容例如root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ...同时页面的样式很可能已经错乱因为我们在一个PHP文件中包含了非PHP的文本文件破坏了原有的HTML结构。失败的可能原因与排查返回了正常的phpMyAdmin页面说明target参数未被处理或者Payload没有触发包含。检查是否在登录状态Cookie是否有效Payload编码是否正确可以尝试%3f单次编码或%23#的编码。尝试另一个白名单文件如import.php。页面报错如500 Internal Server Error这是一个好迹象说明include语句执行了但找不到db_sql.php?/../../....这个文件。这恰恰证明了校验已通过但包含路径错误。可以尝试减少../的层级因为Docker容器内的文件系统路径可能不同。例如尝试targetdb_sql.php%253f/../../../etc/passwd。返回了db_sql.php的源码这说明服务器将%253f解码后PHP并没有将问号后的内容视为路径而是当成了查询字符串。db_sql.php本身被包含并执行了。此时可以尝试使用#锚点其URL编码为%23。Payloadtargetdb_sql.php%23/../../../../etc/passwd。在某些配置下#后的内容在URL中不会被发送到服务器但在构造本地文件路径时可能被PHP以特定方式解析。注意事项在Docker的Linux容器中/etc/passwd文件是存在的。但如果你的Payload一直不成功可以尝试包含一个绝对存在的、有读取权限的web文件来验证漏洞是否触发。例如包含phpMyAdmin自身的配置文件config.inc.php通常位于根目录。Payloadtargetdb_sql.php%253f/../../../../usr/src/phpmyadmin/config.inc.php路径需根据实际容器调整。如果成功响应中可能会泄露数据库密码等敏感信息这同样证明了漏洞的危害性。5. 漏洞利用的进阶场景与防御思考5.1 从文件包含到代码执行读取敏感文件如/etc/passwd、配置文件、日志已经危害很大但文件包含漏洞的终极危害通常是远程代码执行RCE。对于本地文件包含LFI要达成RCE通常需要结合其他条件包含可写的日志文件如果攻击者能控制User-Agent、Referer或访问路径并在其中插入PHP代码然后通过LFI漏洞包含Web服务器如Nginx/Apache的访问日志或错误日志就可能执行代码。但在Docker这种最小化环境中日志可能不可写或路径不固定。包含Session文件PHP的Session文件/tmp/sess_*有时会存储序列化的数据。如果攻击者能预测或控制Session文件的内容例如向某个接收用户输入并存入$_SESSION的页面提交PHP代码再通过LFI包含该Session文件也可能执行代码。包含临时文件上传这是phpMyAdmin这个漏洞更相关的场景。phpMyAdmin有文件上传功能如导入SQL。攻击者可以尝试上传一个包含恶意代码的文本文件然后利用这个文件包含漏洞去包含服务器上的临时上传文件。但这需要知道临时文件的精确路径和文件名难度较高。利用PHP封装协议这是更强大的技巧。即使不能包含远程URLallow_url_include通常为Off也可以利用php://filter协议来读取PHP文件的源码经过Base64编码辅助进行代码审计。例如targetdb_sql.php%253f/../../../../etc/passwd可以尝试替换为targetdb_sql.php%253f/../../../../usr/src/phpmyadmin/index.php但这仍然是文件读取。要执行代码需要能写入文件。如果服务器同时存在文件上传或写入漏洞LFI就能成为最后一步的“点火器”。5.2 漏洞修复方案分析phpMyAdmin官方在后续版本中修复了此漏洞。修复的核心思路是统一校验和包含的路径确保用于校验的字符串和最终被包含的字符串是完全一致的或者对包含路径进行更严格的净化。一种典型的修复方法是在Core::checkPageValidity函数中不仅返回布尔值还通过引用参数返回净化后的、安全的$page值然后index.php包含这个净化后的值。或者在包含之前对$_REQUEST[target]再次进行白名单校验且这次校验必须考虑整个路径或者将路径分解确保其基名basename在白名单内并且路径不能包含目录遍历符。对于开发者而言这个漏洞的教训是输入校验必须彻底校验逻辑必须覆盖所有可能的输入向量和编码形式。校验与使用必须一致用于安全决策的数据必须与最终使用的数据严格一致任何中间处理如截断、解码都可能引入偏差。采用“默认拒绝”策略白名单比黑名单更可靠。但白名单的实现必须严谨像本例中简单的in_array检查在路径拼接场景下就失效了。避免动态包含尽可能避免使用用户输入直接作为文件路径进行包含。如果必须这样做应使用绝对路径映射或强制添加固定的安全前缀后缀。5.3 在企业安全评估中的启发在真实的渗透测试或红队评估中遇到phpMyAdmin、Adminer这类数据库管理工具它们往往是突破口。这个CVE-2018-12613给我们提供了一个检查清单版本识别首先确定phpMyAdmin的版本。可以通过页面底部的版权信息、README文件、js或css文件的版本注释等方式。路径探测尝试访问index.php?target...观察响应。即使没有直接回显通过响应时间、错误信息差异有时也能判断文件是否被尝试包含。利用链思维不要孤立地看一个LFI。它可能和文件上传结合达成RCE。SSRF结合读取内网文件。日志注入结合达成RCE。 结合起来思考才能最大化漏洞的威力。自动化工具验证可以使用如sqlmap它包含对phpMyAdmin此漏洞的检测模块、Metasploit框架中的对应模块进行快速验证。但手动理解和复现是掌握原理不可替代的一步。6. 复现过程中的常见问题与排查在复现过程中你可能会遇到各种“坑”。这里我总结了一份问题排查表涵盖了从环境到利用的各个环节问题现象可能原因排查步骤与解决方案访问http://localhost:8080失败Docker容器未启动或端口冲突1. 运行docker-compose ps检查容器状态。2. 运行docker-compose logs查看日志。3. 检查宿主机8080端口是否被占用netstat -tuln | grep 8080可修改docker-compose.yml中的端口映射。无法登录phpMyAdmin数据库连接失败或凭证错误1. 检查docker-compose.yml中的数据库密码配置。2. 运行docker-compose exec db mysql -u root -p用rootpassword123登录确认数据库服务正常。3. 在phpMyAdmin登录页服务器一栏尝试填写db容器服务名或mysql-for-pma容器名。Burp Suite拦截不到流量浏览器代理未正确设置或Burp监听未开启1. 确认浏览器代理设置为127.0.0.1:8080Burp默认。2. 确认Burp Suite的Proxy - Intercept处于“Intercept is on”状态。3. 检查Burp是否监听在正确的端口Options - Proxy Listeners。发送Payload后返回登录页面会话Cookie失效1. 确保在Burp中修改和转发请求时原始的Cookie请求头没有被删除。2. 重新登录phpMyAdmin获取新的会话Cookie再尝试拦截和修改请求。响应为200 OK但页面正常无文件内容Payload未触发漏洞1.检查白名单文件尝试import.php,export.php等。2.尝试不同编码将%253f换成%3f或%23。3.减少目录穿越层级容器内路径可能较浅尝试../或../../。4.尝试包含一个确定存在的Web文件如targetdb_sql.php%253f./index.php包含当前目录下的index.php。响应为500 Internal Server Error包含路径错误文件不存在1. 这是一个积极信号说明include被执行了。2. 调整路径尝试包含一个绝对存在的文件如phpMyAdmin自身的libraries/common.inc.php。3. 查看Docker容器日志docker-compose logs vulnerable-pma获取具体的PHP错误信息。响应中包含db_sql.php的源码Payload被当作查询字符串处理1. 切换到使用#%23进行尝试targetdb_sql.php%23/../../../etc/passwd。2. 研究是否需要对../进行URL编码%2e%2e%2f但本例中通常不需要。独家避坑技巧使用curl进行快速测试当你对Payload不确定时可以先用curl在命令行快速测试避免Burp的繁琐配置。例如# 先登录获取Cookie这里假设登录后Cookie是phpMyAdminabc123 curl -v http://localhost:8080/index.php?targetdb_sql.php%253f/../../../etc/passwd \ -H Cookie: phpMyAdminabc123观察响应体快速迭代Payload。查看容器内部文件结构如果你不确定目标文件的路径可以进入容器查看docker-compose exec vulnerable-pma /bin/bash # 进入容器后 find / -name *.php 2/dev/null | head -20 # 查找PHP文件 ls -la /usr/src/phpmyadmin/ # phpMyAdmin通常安装在此目录 pwd # 查看当前工作目录开启PHP错误显示为了更好的调试可以临时修改phpMyAdmin的Docker环境配置或在本地搭建环境时在php.ini中设置display_errors On。这样当包含出错时会看到更详细的错误信息有助于判断问题所在。但在生产环境或公开靶场切勿开启。