1. 项目概述一次针对WordPress核心权限的深度剖析最近在整理内部安全审计的案例库翻到了一个挺有意思的老漏洞——WordPress的CVE-2021-21389。虽然这个漏洞的补丁早在2021年初就发布了但它在权限设计上暴露出的问题对于理解Web应用安全尤其是像WordPress这样庞大复杂的CMS系统的安全边界依然有很高的学习价值。这不是一个能直接getshell的高危漏洞而是一个典型的“垂直越权”案例。简单来说就是低权限的用户比如订阅者能执行一些本该只有网站管理员才能做的操作。你可能会想一个三年前的漏洞现在还有啥用其实漏洞复现从来不只是为了“攻击”。对于安全研究人员和开发者通过亲手搭建环境、触发漏洞、分析成因你能最直观地理解权限校验的薄弱点在哪里代码审计时应该关注哪些敏感函数以及如何在自己的项目中避免类似的逻辑缺陷。对于使用WordPress的站长了解这个漏洞能让你更清楚及时更新核心版本的重要性哪怕是小版本更新也可能封堵了某个危险的安全隐患。今天我就带大家从零开始完整地复现一遍CVE-2021-21389。我们会从环境搭建讲起一步步分析漏洞原理并最终演示如何利用一个仅有“订阅者”权限的账户越权修改网站的关键选项。整个过程我会穿插很多实际操作中的细节和避坑点无论你是想入门Web安全的新手还是希望加固自己WordPress站点的管理员相信都能有所收获。2. 漏洞原理与核心逻辑深度拆解在动手之前我们必须先搞清楚这个漏洞到底是怎么回事。CVE-2021-21389被标记为一个“存储型XSS”漏洞但它的根源和影响远不止于此。官方描述是在WordPress 5.7以下的版本中经过身份验证的用户即使权限很低可以通过REST API端点注入恶意JavaScript代码这些代码将在网站后台的“小工具”Widgets管理界面执行。2.1 核心问题REST API的权限校验缺失WordPress从4.7版本开始引入了完整的REST API这为开发者、移动应用和其他系统与WordPress交互提供了极大的便利。这些API端点Endpoint通常对应着不同的资源比如文章/wp/v2/posts、用户/wp/v2/users等。每个端点对于不同的用户角色管理员、编辑、作者、订阅者等有着不同的权限策略。漏洞出在一个用于处理“小工具”Widgets的REST API端点/wp/v2/widgets。小工具是WordPress侧边栏、页脚等区域可拖拽的功能模块比如“最新文章列表”、“搜索框”等。管理小工具包括添加、删除、更新理论上需要edit_theme_options这个权限能力Capability。在WordPress中通常只有管理员Administrator和在某些主题下的编辑Editor才拥有这个权限。然而在受影响版本的WordPress中/wp/v2/widgets端点的权限回调函数permission callback存在逻辑缺陷。具体来说是在更新PUT或POST请求小工具实例Widget Instance时权限检查没有正确验证当前用户是否真正拥有修改特定小工具的权限。它可能只做了非常宽松的检查甚至依赖于客户端传来的、不可信的数据来进行权限判断。注意这里的关键是“小工具实例”。每个小工具类型如“文本小工具”可以有多个实例比如侧边栏一个文本块页脚另一个文本块。漏洞允许低权限用户修改他们本无权访问的、已存在的其他小工具实例的内容。2.2 攻击链从越权修改到存储型XSS漏洞的利用链条可以分为两步垂直越权攻击者首先需要一个低权限账户如订阅者subscriber。订阅者通常只有阅读文章的权限。利用有缺陷的API端点攻击者可以构造一个恶意请求指向一个已经存在的、由管理员创建的小工具实例例如一个“文本小工具”。由于权限校验不严这个请求会被成功执行导致小工具的内容被恶意修改。存储型XSS攻击者将恶意的JavaScript代码作为新的小工具内容提交。例如在文本小工具中插入标签。当具有高权限的管理员登录后台并访问“外观 - 小工具”管理页面时浏览器会加载并渲染所有小工具的预览。此时被篡改的小工具中的恶意脚本就会在管理员的会话上下文中执行。为什么在管理员上下文中执行XSS很危险因为管理员拥有网站的最高权限。通过XSS攻击者可以悄无声息地窃取管理员的后台会话Cookie实现完全接管。利用管理员权限通过JavaScript发起进一步攻击例如创建新的管理员账户、植入后门主题或插件、篡改网站内容等。2.3 影响范围与补丁分析这个漏洞影响WordPress 5.6.2及之前的所有版本。WordPress 5.7版本中修复了此问题。修复的核心是强化了/wp/v2/widgets端点的权限回调逻辑确保在对小工具实例执行更新操作时严格验证当前用户是否拥有edit_theme_options权限并且对操作的对象进行了更严格的访问控制。复现这个漏洞我们选择WordPress 5.6.2这个最后一个受影响的版本。通过对比修复前后的代码我们能更深刻地理解“权限校验”这个安全概念在具体代码中是如何落地和可能如何出错的。3. 实验环境搭建与关键配置为了安全、可控地复现漏洞我们必须在隔离的环境中进行。我推荐使用Docker来快速搭建一套完整的WordPress实验环境这能保证你的主机系统不受影响也方便随时重置。3.1 使用Docker Compose一键部署我们不需要手动配置MySQL和Web服务器一个docker-compose.yml文件就能搞定一切。在你的实验目录下比如~/wordpress_cve创建这个文件version: 3.8 services: db: image: mysql:5.7 volumes: - db_data:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: some_root_password MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress_password networks: - wp_net wordpress: image: wordpress:5.6.2-php7.4-apache # 指定受影响版本 depends_on: - db ports: - 8080:80 # 将主机8080端口映射到容器80端口 restart: always environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress_password WORDPRESS_DB_NAME: wordpress volumes: - wp_data:/var/www/html - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini # 可选用于调整上传限制 networks: - wp_net volumes: db_data: wp_data: networks: wp_net:关键配置解析wordpress:5.6.2-php7.4-apache这里明确指定了包含漏洞的WordPress 5.6.2版本镜像。这是复现成功的前提。ports: - 8080:80你将在本地通过http://localhost:8080来访问WordPress站点。避免使用常见的80端口防止冲突。网络所有服务在一个自定义网络wp_net内数据库主机名直接用服务名db即可访问这是Docker Compose的特性。保存文件后在终端中进入该目录运行命令启动环境docker-compose up -d-d参数表示后台运行。首次运行会下载镜像稍等片刻后访问http://localhost:8080就应该能看到WordPress著名的“五分钟安装”页面了。3.2 WordPress初始安装与用户创建按照安装页面提示完成WordPress的初始化安装。数据库信息已经由docker-compose.yml中的环境变量定义好了直接点击“提交”即可。然后设置站点标题、创建管理员账户记住这个账户和密码我们后面登录后台用。安装完成后以管理员身份登录后台http://localhost:8080/wp-admin。为了复现漏洞我们需要创建一个低权限的用户账户。在左侧菜单栏进入【用户】-【添加新用户】。填写用户名如lowpriv_user。填写一个安全的邮箱可以随意但格式要正确。最关键的一步在“角色”下拉菜单中选择订阅者。订阅者是权限最低的角色通常只能管理自己的个人资料。勾选“发送用户通知”可以取消然后点击“添加新用户”。记下这个低权限用户的用户名和密码。至此我们的实验环境就准备好了一个运行WordPress 5.6.2的网站拥有一个管理员账户和一个订阅者账户。实操心得在Docker环境中有时首次安装后可能会遇到“建立数据库连接时出错”。这通常是MySQL容器还没完全启动好导致的。遇到这种情况只需等待十几秒然后刷新页面即可。使用docker-compose logs db命令可以查看数据库容器的启动日志确认其状态。4. 漏洞复现操作全流程解析现在我们进入核心的漏洞利用环节。我们将模拟一个攻击者在仅拥有订阅者账户的情况下如何利用REST API越权修改小工具并植入XSS payload。4.1 获取必要的认证与目标信息WordPress的REST API大部分端点需要认证。对于这种浏览器外的自动化操作我们通常使用“应用密码”Application PasswordsWordPress 5.6自带或更传统的“Cookie/Session”方式。为了方便演示我们直接使用订阅者账户的登录Cookie。但更接近真实攻击场景的是使用REST API的认证方式这里我们为了简化使用浏览器开发者工具来捕获和重放请求这同样能清晰展示漏洞过程。第一步获取低权限用户的认证Cookie打开浏览器无痕窗口避免现有Cookie干扰访问http://localhost:8080/wp-login.php。使用我们创建的订阅者账户lowpriv_user登录。登录成功后按F12打开开发者工具切换到“网络”Network选项卡。刷新一下页面在请求列表中找到任何一个对/wp-admin/或根目录的请求。查看该请求的“标头”Headers找到Cookie这一长串值复制下来。这里面包含了登录会话信息如wordpress_logged_in_...。第二步探查现有小工具我们需要知道一个现有小工具的ID作为攻击目标。小工具ID的格式通常类似widget_text[2]其中text是小工具类型2是实例编号。以管理员身份在另一个浏览器或标签页登录后台。进入【外观】-【小工具】。默认情况下边栏可能有一个“搜索”小工具和一个“最近文章”小工具。我们以“文本”小工具为例如果你没有可以手动添加一个从左侧可用小工具列表里拖一个“文本”到右侧的某个边栏区域如“主边栏”随便填写一个标题和内容如“测试文本”然后点击“保存”。保存后这个小工具就被分配了一个ID。如何获取这个ID呢一个简单的方法是再次打开浏览器开发者工具的“网络”选项卡然后在小工具页面点击一下“更新”按钮。观察发出的POST请求在请求参数或URL中你可能会找到类似widget-text[2]这样的字段。更直接的方法是查看页面HTML源码搜索widget-text但通过API利用时我们有时需要另一种形式的ID。实际上对于REST API小工具的ID是其“基ID”base ID和实例编号的组合。但在这个漏洞的利用中我们可以通过API列出所有小工具来获取信息。不过列出小工具可能也需要较高权限。因此在实际攻击中攻击者可能会进行猜测枚举常见的ID或者结合其他信息泄露漏洞。为了本次演示我们假设攻击者通过某种方式比如另一个低权限的信息泄露得知了一个文本小工具的ID是text-2。4.2 构造并发送恶意请求现在我们扮演攻击者使用订阅者的会话Cookie向REST API发送一个恶意请求来修改ID为text-2的小工具。我们将使用一个命令行工具curl来发送这个请求这能更清晰地展示请求的原始面貌。请确保你的终端可以访问localhost:8080。构造恶意Payload 我们打算在小工具内容里插入一个简单的XSS payload例如scriptalert(XSS by lowpriv_user);/script。发送PUT请求 REST API更新小工具的端点是PUT /wp-json/wp/v2/widgets/widget_id。我们需要将Cookie和JSON数据一起发送。打开你的终端执行以下命令请将你的Cookie替换为之前复制的订阅者用户的完整Cookie字符串curl -X PUT http://localhost:8080/wp-json/wp/v2/widgets/text-2 \ -H Content-Type: application/json \ -H Cookie: 你的Cookie \ -H X-HTTP-Method-Override: PUT \ -d { id: text-2, id_base: text, settings: { text: scriptalert(\XSS by lowpriv_user\);/script }, instance: { encoded: xxxxxxxx, # 这个值通常可以从现有小工具的API响应中获得但有时不是必须的 hash: yyyyyyyy, raw: false } }命令参数解析-X PUT指定使用PUT方法。-H Content-Type: application/json告诉服务器我们发送的是JSON数据。-H Cookie: ...携带低权限用户的认证Cookie这是通过权限校验尽管有缺陷的关键。-H X-HTTP-Method-Override: PUT有些服务器配置可能对PUT方法支持不完整这个头可以确保方法被正确识别。-d ...这是请求体包含了我们要更新的小工具数据。其中settings.text字段被我们注入了恶意脚本。重要注意事项上面的JSON数据是一个简化示例。在实际的WordPress 5.6.2 API中更新小工具所需的字段结构可能更复杂instance.encoded和instance.hash的值必须与服务器端当前存储的该小工具实例的状态匹配否则请求会失败。攻击者如何获取这些值他们可能需要先通过一个GET请求如果该端点存在信息泄露来获取这个小工具的完整JSON表示从中提取instance对象然后再修改settings并发起PUT请求。这就是一个完整的“读取-修改-写入”越权攻击链。为了简化演示我们假设攻击者已经通过其他途径获取了正确的instance数据。简化攻击模拟 由于直接构造完整的PUT请求较复杂我们可以换一个更直观的方法来验证漏洞的“越权”本质尝试通过API创建一个新的小工具实例。创建操作同样可能受到有缺陷的权限校验影响。创建小工具使用POST /wp-json/wp/v2/widgets端点。我们可以尝试用订阅者身份创建一个文本小工具curl -X POST http://localhost:8080/wp-json/wp/v2/widgets \ -H Content-Type: application/json \ -H Cookie: 你的Cookie \ -d { id_base: text, settings: { title: Hacked Widget, text: scriptalert(\Subscriber Created This!\);/script } }如果这个请求返回成功HTTP状态码200或201并且响应中包含了新创建的小工具ID那么就强烈表明权限校验存在问题因为订阅者不应该拥有创建小工具的权限edit_theme_options。4.3 验证攻击效果管理员触发XSS无论我们通过上述哪种方式越权修改现有小工具或越权创建新小工具成功了下一步就是等待或诱导管理员触发XSS。确保你已用管理员账户登录了WordPress后台在另一个浏览器标签页。导航至【外观】-【小工具】。如果漏洞利用成功你添加了恶意脚本的小工具无论是被篡改的还是新创建的会出现在小工具管理列表中。当管理员页面加载并渲染这个小工具的预览时嵌入的JavaScript代码就会执行。在我们的例子中会弹出一个警告框显示“XSS by lowpriv_user”或“Subscriber Created This!”。成功弹窗意味着什么这不仅仅是一个弹窗演示。它证明了垂直越权成功一个订阅者角色执行了需要管理员权限的操作修改/创建小工具。存储型XSS成功恶意代码被持久化保存在数据库中并在高权限用户访问特定页面时执行。攻击链成立结合两者攻击者可以在管理员不知情的情况下在后台植入恶意脚本为进一步的渗透如窃取会话、创建后门等打开大门。5. 漏洞根因分析与代码审计视角复现成功后我们不能只停留在“能攻击”的层面更要理解代码层面到底哪里出了问题。这能帮助我们未来在审计其他应用时具备类似的“嗅觉”。5.1 定位问题代码WordPress的核心代码位于/wp-includes/目录。REST API相关的代码主要在/wp-includes/rest-api/。小工具相关的REST API端点注册我们可以在/wp-includes/widgets.php或/wp-includes/rest-api/endpoints/目录下寻找。在WordPress 5.6.2中负责注册/wp/v2/widgets端点的代码位于/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php。我们可以查看其permission_callback方法。关键代码分析简化概念版 在漏洞版本中update_item_permissions_check处理PUT/PATCH请求的权限检查函数可能存在类似以下的逻辑缺陷function update_item_permissions_check( $request ) { $widget_id $request[id]; // 错误逻辑可能只检查了用户是否有“编辑文章”等无关权限 // 或者从$request参数中读取了某个本应由服务器端确定的“权限标志”并信任了它 // 例如错误地依赖了客户端传来的 $request[capability] 来判断 if ( current_user_can( edit_posts ) ) { // 订阅者没有此权限但作者有这里检查可能太宽泛或错误 return true; } // 正确的逻辑应该严格检查主题选项编辑权限 // return current_user_can( edit_theme_options ); return false; }或者问题可能出在register_rest_route注册端点时没有正确设置permission_callback导致它默认了一个不安全的权限检查。5.2 修复方案解读在WordPress 5.7的修复中开发团队强化了这些权限回调函数。修复后的代码会明确地、严格地检查edit_theme_options权限并且在对小工具实例进行任何操作前会验证该实例是否确实属于当前用户有权操作的范畴虽然小工具通常是全局的但这里更强调了权限的绝对性。查看官方提交记录修复通常涉及确保create_item_permissions_check,update_item_permissions_check,delete_item_permissions_check等方法都正确调用了current_user_can( edit_theme_options )。对输入参数进行更严格的验证和清理防止通过参数污染进行权限绕过。可能引入了对instance哈希值的强制校验确保客户端不能随意篡改一个现有小工具的核心标识属性。5.3 审计经验总结从这个漏洞我们可以学到几点Web安全审计的经验关注权限回调在审计任何API、控制器函数时第一眼就要找到它的权限检查入口permission_callback,check_permissions,can等方法。验证其使用的权限能力Capability是否与操作的危险级别匹配。理解“信任边界”服务器绝不能信任客户端传来的任何关于权限、身份判定的数据。像“role”、“capability”、“user_id”这类字段必须由服务器端会话状态决定。细粒度访问控制不仅要检查“能否操作这个类型”还要检查“能否操作这个特定实例”。虽然在小工具这个案例里实例的归属比较全局化但在文章、用户数据等场景下必须验证对象的所有权。旧版组件风险WordPress的小工具系统是一个较老的模块当其被整合到新的REST API架构时很容易产生新旧代码之间的权限校验缝隙。审计时要特别留意这种“老功能新接口”的组合。6. 防御措施与安全加固建议复现和分析漏洞的最终目的是为了更好地防御。针对CVE-2021-21389这类漏洞我们可以从多个层面进行防护。6.1 对于WordPress管理员站长立即更新这是最根本、最有效的措施。确保你的WordPress核心、主题、插件始终保持最新版本。WordPress 5.7及以上版本已修复此漏洞。最小权限原则只给用户分配完成其工作所必需的最低权限。不要随意给用户提升角色。如果某人只需要写文章就不要给他“编辑”或“管理员”角色。审计用户与插件定期检查网站上的用户账户删除不再使用的账户。审慎安装插件只从官方目录或可信来源获取并保持插件更新。一个劣质插件可能本身就会引入类似的权限漏洞。使用安全插件可以考虑安装WordPress安全插件如Wordfence, Sucuri等它们通常包含防火墙、恶意软件扫描和登录尝试限制等功能能增加一层防护。监控与日志关注网站的错误日志和访问日志留意是否有异常的REST API请求特别是对/wp-json/wp/v2/widgets端点的非管理员访问。6.2 对于开发者遵循核心安全API在开发主题或插件时凡是涉及权限判断务必使用WordPress提供的API如current_user_can()并传递正确的权限能力名称。非公开功能需显式校验如果你创建了自定义的REST API端点或AJAX处理函数一定要显式地、在最开始进行权限校验不要依赖前端隐藏或UI限制。输入验证与输出转义对所有用户输入进行验证和清理对所有输出到前端的数据进行适当的转义使用esc_html(),esc_attr()等函数防止XSS。进行代码审查在团队开发中将权限检查和安全处理作为代码审查的必查项。6.3 漏洞复现环境的安全处置实验完成后务必妥善关闭和清理环境避免留下存在漏洞的系统。停止并移除容器在docker-compose.yml所在目录下运行docker-compose down -v-v参数会同时删除创建的命名卷db_data和wp_data彻底清除所有数据。确认清理运行docker ps -a和docker volume ls确认相关的容器和卷已被移除。安全意识永远不要在公网服务器上运行存在已知高危漏洞的旧版软件进行“测试”除非你将其置于严格隔离的网络环境中如VPN之后并有严格的访问控制。否则它很可能在几分钟内被自动化攻击脚本攻陷。7. 常见问题与排查技巧实录在复现过程中你可能会遇到一些问题。这里我总结了一些常见的坑和解决方法。7.1 环境搭建与访问问题问题1访问localhost:8080显示“无法连接”或“连接被拒”。排查首先运行docker-compose ps查看两个服务db和wordpress的状态是否为“Up”。如果状态不对运行docker-compose logs查看具体错误日志。常见原因端口冲突。主机上的8080端口可能已被其他程序如其他开发服务器占用。修改docker-compose.yml文件中的端口映射例如将8080:80改为8081:80然后重新运行docker-compose up -d。Docker Desktop用户确保Docker Desktop已启动。在Windows/Mac上有时需要显式在终端中启动Docker服务。问题2WordPress安装页面提示“建立数据库连接时出错”。排查这是最常见的问题。99%的原因是MySQL容器尚未完成初始化。WordPress容器启动速度比MySQL快。解决耐心等待30-60秒然后刷新浏览器页面。可以通过docker-compose logs db命令查看MySQL日志当看到类似“MySQL init process done. Ready for start up.”或“mysqld: ready for connections”的日志时说明数据库已就绪。7.2 漏洞复现请求失败问题3发送的curl请求返回401或403状态码。401 Unauthorized认证失败。请仔细检查复制的Cookie字符串是否完整、正确。确保是在订阅者用户登录后复制的并且没有过期。可以尝试在无痕窗口中重新登录订阅者账户再复制一次。403 Forbidden权限不足。这可能有几种情况你使用的WordPress镜像版本不对可能已经打了补丁。务必确认是wordpress:5.6.2-php7.4-apache。你尝试操作的小工具ID不正确或者instance数据不匹配。尝试先通过管理员账户的REST API请求在浏览器中访问http://localhost:8080/wp-json/wp/v2/widgets可能需要使用管理员Cookie或应用密码来获取准确的小工具列表和其完整的JSON结构。权限校验在某些特定条件下可能仍会生效。尝试使用更“简单”的POST创建请求来测试越权如前文所述。问题4请求返回404状态码。排查REST API端点路径错误。确保URL为http://localhost:8080/wp-json/wp/v2/widgets。WordPress的REST API根路径是/wp-json/。检查固定链接如果站点固定链接设置是“朴素”模式可能会影响REST API的访问。建议在后台【设置】-【固定链接】中选择除“朴素”以外的任何格式如“文章名”然后保存。这通常不会影响API但能确保.htaccess规则正常。问题5管理员访问小工具页面没有弹窗。排查首先确认curl请求是否返回了成功状态码200或201。如果成功检查响应内容看是否真的创建或更新了小工具。浏览器因素现代浏览器如Chrome、Firefox的内置XSS过滤器XSS Auditor或类似机制可能会阻止简单的alert()弹窗。可以尝试更隐蔽的payload例如然后在开发者工具的“控制台”(Console)中查看是否有输出。小工具位置确保你修改或创建的小工具被分配到了当前主题正在使用的“边栏”Sidebar或“小工具区域”Widget Area中。如果小工具只是被创建但未添加到任何活动区域管理员在“小工具”管理页面可能看不到它。你需要通过API或手动在后台将其拖入一个活动区域。7.3 漏洞利用的深入与变种问题6除了弹窗这个漏洞还能做什么窃取Cookie将payload替换为 其中your_server是你控制的服务器。当管理员加载页面时其Cookie会被发送到你的服务器。执行管理操作可以构造更复杂的JavaScript利用管理员的会话通过Fetch API向WordPress后台的其他端点如创建用户、安装插件、修改设置发起AJAX请求。例如发送一个请求到/wp-admin/user-new.php来添加一个攻击者控制的管理员账户。键盘记录注入键盘记录脚本窃取管理员输入的后台密码或其他敏感信息。问题7这个漏洞容易被大规模利用吗有一定门槛。攻击者需要先获得一个低权限的账户订阅者。这可以通过注册如果网站开放注册、社工、或利用其他漏洞如弱口令来实现。一旦获得立足点利用此漏洞可以升级权限危害较大。在修复前所有未更新且开放用户注册的WordPress 5.6.2站点都面临风险。整个复现过程走下来给我的最深体会是安全是一个链条任何一个环节的疏忽比如一个API端点权限检查的遗漏都可能被串联起来导致整个防线失守。对于开发者绝不能对客户端传来的数据有任何信任对于运维人员及时更新不仅是修复功能更是堵上这些看不见的安全缺口。手动复现一遍比看十篇分析文章都记得牢。下次再审计代码时看到permission_callback我肯定会多盯它两眼。