
CVE-2026-64638 的概念验证漏洞利用:WordPress 登录中的反射型 XSS 与 DOM clobbering 链式利用,可实现管理员账户接管和远程代码执行。
软件: WordPress Core ≤ 7.0.2(7.0.3 之前的所有版本)
CVSS: 8.9(高危)
CWE: CWE-79——网页生成期间输入未正确中和
所需认证: 无(预认证)
用户交互: 主动(管理员需点击 1 个链接)
影响: XSS → 账户接管 → 远程代码执行
WordPress 是全球最流行的内容管理系统,占互联网上所有网站的 40% 以上。每个 WordPress 站点都有一个位于 /wp-login.php 的登录页面——这是一个公共端点,任何人都可以在无需认证的情况下访问。
当用户输入错误的用户名时,WordPress 会显示一条包含用户刚输入的确切用户名的错误消息:“用户名 X 未在此站点注册。” 问题在于,用户名值被直接放入 HTML 响应中,未经过任何转义函数——攻击者只需输入 HTML/JavaScript 而不是真实用户名,代码就会在浏览器中执行。
这是一个反射型 XSS 漏洞——载荷包含在请求中,服务器将其原样反射回 HTML 中。其危险之处在于该漏洞位于登录页面——管理员经常访问的地方,管理员的会话 Cookie 可能在此被窃取。
研究团队进一步发现,此 XSS 可与 WordPress emoji-loader 中的 DOM clobbering(DOM 覆写)漏洞链式利用,从而允许从外部服务器加载 JavaScript。以此为基础,攻击者可以创建新的管理员账户 → 安装包含 WebShell 的插件 → 在服务器上执行 PHP 代码。此利用链被称为 XSS2Shell。
| 属性 | 值 |
|---|---|
| CVE 编号 | CVE-2026-64638 |
| CVSS 评分 | 8.9(高危) |
| 软件 | WordPress Core ≤ 7.0.2 |
| 认证 | 无需认证(预认证) |
| 用户交互 | 需要 1 次点击(管理员点击链接) |
| 攻击复杂度 | 高 |
| 已修复版本 | WordPress 7.0.3(08/06/2026) |
| 报告者 | pwn.ai 团队,通过 HackerOne |
| HackerOne 报告 | #3877102 |
WordPress 通过文件 emoji-loader.js 在每个页面(包括登录页面)加载 emoji 支持。此脚本从具有 id="wp-emoji-settings" 的元素中读取配置:
// Before patch (vulnerable)
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
document.getElementById() 返回 DOM 中具有匹配 id 的第一个元素。如果攻击者在原始 script 标签之前注入一个 <div id="wp-emoji-settings">,getElementById 将读取攻击者的内容而不是真实配置。这种技术称为 DOM clobbering——通过注入 HTML 元素来覆盖 JavaScript 行为。
emoji 配置包含一个用于加载 JavaScript 文件的 URL(concatemoji)。攻击者控制此 URL → 从外部服务器加载 JS 文件 → 在浏览器上下文中执行任意代码。
一旦在管理员上下文中实现 JavaScript 执行,攻击者就拥有完整的 WordPress 管理员权限:
/wp-admin/user-new.php/wp-admin/plugin-install.php 上传插件以上 3 种方法中的任何一种都允许在服务器上执行 PHP 代码——即 RCE。
从 wordpress-develop 上的修复提交 0d6d42e 中,我确定了 wp-includes/user.php 文件中 3 个将用户名/邮箱直接放入错误消息的位置:
# View diff between vulnerable and patched versions
git diff 7.0.2..7.0.3 -- src/wp-includes/user.php
位置 1——第 189 行(用户名不存在):
修复前:
// BEFORE (vulnerable):
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
$username // ← no escaping
)

修复后:
// fixed:
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
esc_html( $username ) // ← escaped
)
位置 2——第 216 行(密码错误):
修复前:

// BEFORE:
'<strong>' . $username . '</strong>' // ← no escaping
修复后:
// AFTER:
'<strong>' . esc_html( $username ) . '</strong>'
位置 3——第 299 行(邮箱密码错误):
修复前:

// BEFORE:
'<strong>' . $email . '</strong>' // ← no escaping
修复后:
// AFTER:
'<strong>' . esc_html( $email ) . '</strong>'
从 POST 请求到错误消息的数据流:
$_POST['log'] ← user input from login form
↓
wp_signon() [user.php:51]
$credentials['user_login'] = wp_unslash($_POST['log']) ← only removes backslashes
↓
wp_authenticate($username, $password) [pluggable.php:689]
$username = sanitize_user($username) ← strips HTML tags, but has a bypass
↓
wp_authenticate_username_password() [user.php:153]
get_user_by('login', $username) ← user not found
↓
sprintf('The username <strong>%s</strong>...', $username) ← XSS!
↓
WP_Error → login_header() → wp_admin_notice()
wp_kses_post(...) ← filters HTML but allows <div>, <a>, through
↓
HTML response → browser render → JavaScript execute
在用户名到达 HTML 之前,WordPress 有 2 层过滤:
第 1 层:sanitize_user()——调用 strip_tags() 移除 HTML 标签。然而,PHP 的 strip_tags() 存在已知限制:非标准标签格式可以绕过过滤器。
第 2 层:wp_kses_post()——允许安全 HTML 子集通过,包括带有某些属性的 <div>、<a>、``(但会剥离 onerror、onload 等事件处理属性)。关键点:wp_kses_post 允许 <div id="wp-emoji-settings">——这正是 DOM clobbering 所需的元素。
pwn.ai 团队找到了一种绕过这两层过滤以注入有效载荷的方法。具体技术细节尚未公开披露。
为了直观确认用户名未经转义就直接进入 HTML,我使用 Xdebug + VS Code 在执行链的关键位置设置断点。
第 1 步——在登录表单中输入 XSS 载荷:
访问 http://localhost:8282/wp-login.php,输入用户名 ``,然后点击“登录”。出现弹窗——XSS 生效。


第 2 步——在 user.php:184 设置断点——XSS 发生的位置:
在函数 wp_authenticate_username_password() 内的 return new WP_Error(...) 处设置断点。当调试器暂停时,观察:
$username = ""——完整的 HTML 载荷,未转义$_POST:log = ""——确认载荷源自表单输入wp_authenticate_username_password() → WP_Hook->apply_filters → apply_filters → wp_authenticate → wp_signon → {main} wp-login.php
$username 的值从 $_POST['log'] → wp_unslash() →(绕过 sanitize_user)→ 通过 sprintf() 进入第 186-189 行的错误消息——中间没有任何 esc_html()。在实验室中,sanitize_user() 被注释掉,以模拟 pwn.ai 发现的绕过方式。
第 3 步——在 functions.php:9200 设置断点——最终输出:
在 echo wp_get_admin_notice( $message, $args ) 处设置断点——这是 HTML 输出到浏览器之前的最后一行:
$message = "<p><strong>Error:</strong> The username <strong></strong> is not registered...</p>"——载荷完整地存在于 HTML 错误消息中wp_kses_post() 包裹(已注释以模拟绕过),因此载荷直接进入浏览器
在原始 WordPress 中,这一行是 echo wp_kses_post( wp_get_admin_notice(...) )——wp_kses_post() 会剥离 onerror 属性,但会允许 <div id="wp-emoji-settings"> 通过,因为 <div> 在白名单中。这正是 DOM clobbering 攻击的精准向量。
提交 a12c8f5 修改了 emoji-loader.js 以阻止 DOM clobbering:
// BEFORE (vulnerable): accepts any element with a matching id
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
// AFTER (fixed): only accepts <script> element
const selector = 'script#wp-emoji-settings';
const script = document.querySelector(selector);
if (!(script instanceof HTMLScriptElement)) {
throw new Error(`Element missing:${selector}`);
}
const settings = JSON.parse(script.text);
此修复更改了 3 处:
querySelector('script#...') 代替 getElementById——只匹配 <script> 标签instanceof HTMLScriptElement——防止通过 <div> 或 `` 进行 DOM clobbering.text 代替 .textContent——.text 是 HTMLScriptElement 的特定属性修复后,即使攻击者设法注入 <div id="wp-emoji-settings">,emoji-loader 也会忽略它,因为它不是 <script> 元素。