针对 CVE-2026-18322 的根因分析、被动版本检测器及实验环境 PoC,该漏洞是 Smart Popup by Supsystic WordPress 插件中的未认证权限提升漏洞。
针对 CVE-2026-18322 的安全研究,这是 WordPress 插件 Smart Popup by Supsystic(popup-by-supsystic)中的一个未认证权限提升漏洞。本仓库包含基于上游源码差异的根因分析、提取出的补丁、用于识别受影响安装的检测工具(被动版本指纹识别器),以及一个用于漏洞利用 PoC 的实验环境。
该漏洞已公开且已修复。本工作成果以防御性用途发布:帮助运维人员发现并修复受影响的主机。
| 版本范围 | 状态 |
|---|---|
< 1.13.0 | 存在漏洞 |
>= 1.13.0 | 已修复 |
1.13.0(发布于 2026 年 7 月 31 日)是首个修复版本;它修复了下文所述攻击链的全部三个环节。参见分析文档第 1 节。
三个相互独立的弱点组合成了未认证创建管理员账户的攻击:
1. 权限映射冲突 — havePermissions() 使用 array_merge() 合并两个权限映射。两者都使用字符串键 PPS_USERLEVELS('userlevels'),而 array_merge() 会覆盖字符串键,因此基础控制器的简短默认列表整体替换了 popup 模块的列表——悄无声息地移除了 save 及其他九个方法的管理员限制:
$permissions = $mod->getController()->getPermissions(); // [... 'save' ...]
$permissionsBase = $mod->getController()->getBasePermissions(); // ['getListForTbl','removeGroup','clear']
$permissions = array_merge($permissions, $permissionsBase); // base wins — 'save' is gone
该检查以开放方式失败:映射中不存在的操作永远不会被拒绝。
2. 可复用的 nonce 被邮寄给陌生人 — save 仍然要求 pps_nonce,但订阅确认邮件中嵌入了完全相同的 nonce action。对于未登录用户,WordPress nonce 以 uid=0 为键,因此为匿名订阅者生成的令牌对任何匿名攻击者都有效。
3. 没有服务端角色允许列表 — createWpSubscriber() 将配置的角色直接传递给 WP_User::set_role()。插件的安全角色列表(其中排除了 administrator)仅在渲染管理下拉菜单时应用——这只是 UI 层面的控制。
串联利用:订阅以获取 nonce → 将其重放至 popup::save 以设置 sub_wp_create_user_role=administrator → 触发订阅流程 → 获得持久化管理员账户。
包含文件/行号引用的完整演练:docs/ANALYSIS.md。
poc/cve_2026_18322_check.py 用于判断站点是否运行受影响的版本。它从不尝试漏洞利用:它只发送普通 HTTP GET 请求,并读取安装程序对外公布的版本信息。
git clone <this-repo> && cd CVE-2026-18322
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
requests 是推荐但可选的——该工具会回退到标准库。
# Single target
python3 poc/cve_2026_18322_check.py https://example.com
# With supporting evidence
python3 poc/cve_2026_18322_check.py https://example.com -v
# Many targets, concurrently, exporting results
python3 poc/cve_2026_18322_check.py -f targets.txt -t 20 --json results.json
python3 poc/cve_2026_18322_check.py -f targets.txt --csv results.csv --only-vulnerable
# Through a proxy, ignoring TLS errors (lab use)
python3 poc/cve_2026_18322_check.py https://staging.internal -k --proxy http://127.0.0.1:8080
目标可以作为参数给出,也可以通过 -f 指定(- 表示从标准输入读取)。仅给出主机名时默认使用 https://。
$ python3 poc/cve_2026_18322_check.py https://example.com -v
[VULNERABLE] https://example.com/ (Smart Popup by Supsystic 1.11.2) < 1.13.0 affected by CVE-2026-18322; upgrade to 1.13.0+
- asset-version: plugin asset enqueued with ?ver=1.11.2 (https://example.com/)
- homepage-reference: references /plugins/popup-by-supsystic/ (https://example.com/)
- readme: plugin readme.txt is publicly readable (https://example.com/wp-content/plugins/popup-by-supsystic/readme.txt)
- readme-stable-tag: Stable tag: 1.11.2 (.../readme.txt)
退出码:0 = 没有存在漏洞的目标,1 = 至少有一个存在漏洞,2 = 用法错误。
两个被动信号,均为对公开资源的普通 HTTP GET 请求:
?ver=PPS_VERSION(classes/frame.php:410,473),因此首页常常会泄露确切版本。readme.txt — Stable tag: 具有权威性,优先于可能过期的缓存资源;变更日志作为回退。首页扫描还会发现被重命名的 wp-content 目录(例如 Bedrock 的 /app/),因此 readme 查找会遵循站点的实际布局。
检测工具不会尝试漏洞利用。 此处未发布针对此 CVE 的武器化漏洞利用:唯一运行完整攻击链的代码被硬性限制在本地实验环境中(参见下文)。
被动检查器仅执行版本指纹识别。其 HttpClient 除 GET 外不暴露任何 HTTP 方法——这在构造上得到保证,并在测试套件中加以断言,因此即使误操作也无法发出改变状态的请求。它从不调用 popup::save、不发送 sub_wp_create_user_role 参数、不提交订阅表单、不触发确认邮件,也不创建或修改任何用户、弹窗或设置。它只读取两个公开资源——首页和 readme.txt——除此之外别无其他。
这种安全性是有代价的,需要明确说明:判定结果的好坏仅取决于主机公布的版本元数据。权衡与失效模式:第 9 节。
lab/exploit_full_chain.py 是此处唯一执行实际攻击的代码,它会创建一个 WordPress 管理员账户。发布它是为了让分析可以被复现,而不是仅凭信任接受——一个无法演示的授权漏洞主张只是断言,而非发现。它专为 lab/ 中的一次性技术栈编写,不适用于其他任何环境:
--lab-confirm 为强制参数。 两项检查都在第一个 HTTP 请求之前于 assert_lab_target() 中运行,因此被拒绝的调用完全不会接触目标。http://localhost:8025/api/v1/…)从订阅确认邮件中获取可复用的 pps_nonce。不存在从其他任何地方获取该邮件的代码路径,因此该攻击链对实验环境之外的主机没有第一步。lab/docker-compose.yml 中的每个服务都绑定到 127.0.0.1。因此,按发布状态它无法被指向真实站点——它会在发出请求之前以 REFUSING TO RUN 停止:
$ python3 exploit_full_chain.py https://example.com --lab-confirm
REFUSING TO RUN: example.com resolves to non-local address(es) 104.20.23.154, ...
This script creates a WordPress administrator and is for the local lab only.
To test a real site, use the non-destructive checker in ../poc/.
这些防护是承重结构而非装饰:该脚本就是真实的攻击链,而使其保持为演示的是它不会在一次性本地技术栈之外运行。请不要移除它们,并参阅 SECURITY.md——削弱这些防护的改动,或旨在实验环境之外运行的独立漏洞利用,均超出本仓库的范围。要查明真实站点是否受影响,请使用 poc/——检查器在不接触任何东西的情况下回答同样的问题。
对于您所控制的主机,要获得权威答案:
grep -n "array_merge(\$permissions, \$permissionsBase)" \
wp-content/plugins/popup-by-supsystic/classes/frame.php
任何输出都意味着该主机存在漏洞——那一行正是 1.13.0 所替换的内容。
仅可用于您拥有或获得明确授权测试的系统。参见 SECURITY.md。
lab/ 包含一个一次性 Docker 技术栈,可端到端运行该漏洞,因此分析可以被验证,而不是仅凭信任接受:
cd lab
./setup.sh # WordPress + plugin 1.12.0
../.venv/bin/python exploit_full_chain.py http://localhost:8080 --lab-confirm
./verify.sh # confirm from the database
./teardown.sh
其最有用的输出是版本对比——对三个版本发出完全相同的请求:
./setup.sh --plugin-version 1.11.2 # exploit succeeds
./setup.sh --plugin-version 1.12.0 # exploit succeeds
./setup.sh --plugin-version 1.13.0 # exploit fails at phase 2
该实验环境是故意存在漏洞的,其漏洞利用脚本会创建一个 WordPress 管理员账户。 所有内容都绑定到
127.0.0.1,且该脚本不会针对本地实验环境之外的任何目标运行——参见 实验环境漏洞利用仅在实验环境中运行。它不是扫描器;要检查真实站点,请使用poc/。
sub_wp_create_user_role 被设置为特权角色的弹窗,以及携带 pl=pps&mod=popup&action=save 发往 admin-ajax.php 的 POST 请求。查询语句与日志 grep:第 10 节。
.
├── README.md
├── SECURITY.md Scope, authorized-use policy, disclosure
├── LICENSE MIT
├── requirements.txt
├── docs/
│ └── ANALYSIS.md Full root-cause analysis and fix rationale
├── patches/
│ ├── 01-frame-permission-merge.diff array_merge() → _mergePermissions()
│ ├── 02-subscribe-role-allowlist-and-nonce.diff Role allowlist + dedicated nonce
│ └── 03-v1.12.0-confirmation-page-hardening.diff Unrelated hardening added in 1.12.0
├── poc/
│ └── cve_2026_18322_check.py Passive version fingerprint (GET only)
├── lab/ Disposable Docker lab — INTENTIONALLY VULNERABLE
│ ├── docker-compose.yml MariaDB + WordPress 1.12.0 + Mailpit (loopback only)
│ ├── setup.sh / teardown.sh Bring the lab up / destroy it
│ ├── exploit_full_chain.py Full attack chain — CREATES AN ADMIN; lab-gated
│ └── verify.sh Independent confirmation from the database
├── scripts/
│ └── fetch_versions.sh Export plugin versions from SVN and regenerate diffs
└── tests/
└── test_check.py Offline unit tests for the passive checker
pip install -r requirements.txt pytest
python3 -m pytest tests/ -v
该测试套件完全离线——它使用录制的测试夹具,从不使用实时目标。它覆盖了跨越 1.13.0 边界的版本判定、目标规范化,以及决定每种状态的证据规则。
安全属性是被断言的,而不仅仅是文档化的:完整扫描不会发出改变状态的请求,且 HttpClient 除 GET 外不暴露任何 HTTP 方法。
开发过程中发现的两个回归问题已被锁定:CRLF 格式的 readme.txt 会破坏基于行锚点的解析,以及 HTTP 错误页面被误计为插件证据。
要从上游源码复现分析:
./scripts/fetch_versions.sh # exports 1.11.2 / 1.12.0 / 1.13.0 and regenerates patches/
MIT — 参见 LICENSE。所分析的插件为 GPLv2-or-later,不在此处再分发;scripts/fetch_versions.sh 会从官方 WordPress SVN 仓库获取它。
| 状态 | 含义 |
|---|
VULNERABLE | 检测到插件版本为 < 1.13.0 |
NOT_VULNERABLE | 检测到插件版本为 >= 1.13.0 |
PLUGIN_DETECTED_VERSION_UNKNOWN | 插件存在,但无法确定版本——请手动核实 |
PLUGIN_NOT_DETECTED | 没有插件存在的证据(不能证明其不存在) |
ERROR | 目标不可达或格式错误 |