CVE-2026-41940深度分析影响7000万+量级的cPanel/WHM认证绕过漏洞:从CRLF注入到匿名RCE

admin 2026-05-06 05:52:29 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: CVE-2026-41940是cPanel/WHM中一个高危认证绕过漏洞,攻击者通过CRLF注入伪造session字段绕过密码与双因素认证,直接获取ROOT权限并实现RCE。漏洞成因是set_pass函数未过滤回车符,且saveSession未调用CRLF过滤函数。影响全球超7000万设备,建议及时更新补丁。 综合评分: 87 文章分类: 漏洞分析,WEB安全,渗透测试,红队,内网渗透


cover_image

CVE-2026-41940 深度分析影响7000万+量级的cPanel/WHM认证绕过漏洞:从CRLF注入到匿名RCE

原创

KCyber KCyber

自在安全

2026年5月5日 07:01 北京

在小说阅读器读本章

去阅读

欢迎大家关注自在安全公众号。为更好学习交流,建了个技术交流群,大家可以扫描进群。你也可以关注公众号后@我拉你进群。

cPanel/WHM 是一套基于 Linux 的虚拟主机管理面板,提供网站和服务器管理功能,号称全球装机数量超过 7000 万。CVE-2026-41940 是一个影响 cPanel/WHM 的严重认证绕过漏洞,该漏洞可以直接绕过所有密码和双因素认证,获取系统 ROOT 权限。

进程与认证

2087 端口对应 cPanel/WHM 后端管理服务,对应进程名为 cpsrvd-dormant ,这是一个 Perl 解释器嵌入的程序:

继续分析发现 cpsrvd-dormant 只是一个 launcher,实际处理逻辑位于 cpsrvd 之中。在服务器的 perl 脚本中并没有找到核心认证函数,逆向分析 cpsrvd 发现多个认证相关函数,比如 handle_auth 、 handle_one_connection 等,这些函数均编译在二进制内部,使用静态反编译无法获取处理源码。为了获取实际处理代码,可以采用 GDB attach 到运行中的 cpsrvd 子进程,在 Perl 解释器运行时完成源码还原。这个过程让 AI 来辅助分析效率很高:

获取源码后,我们才能真正完整弄明白整个认证过程。

form 表单认证

form 表单认证数据包如下:

POST /login/?login_only=1 HTTP/1.1
Host: 127.0.0.1:2087

user=test&pass=test&goto_uri=%2F

处理函数为 handle_form_login ,首先检查请求路径是否为登录 URL :

my $document = $server_obj->request->get_document;
if ($document =~ //u) {   # 匹配 /login
    # todo
}

进入 form login 分支,解析 POST 请求 body :

my $login_form_ref = $server_obj->timed_parseform(60);

调用 Cpanel::Form::parseform 解析表单数据。密码验证失败会调用 badpass 函数,在返回 401 之前仍然会创建 session 并返回对应的 Cookie 值 whostmgrsession :

sub badpass {
    my (%OPTS) = @_;
    ...
    note_cphulk_failure(...) if $OPTS{'notefailure'};

    if (!$session && !$OPTS{'skip_session_creation'}) { # [1]
        my $new_session = Cpanel::Session::create(
            'user'       => '',
            'pass'       => '',
            'needs_auth' => 1,
            'ip_address' => $ENV{'REMOTE_ADDR'},
        );
        # 通过 Set-Cookie 返回给客户端
        # Cookie 值包含 ,obhex 后缀
    }

    # 返回 401 响应
    send_401_response(...);
}

create 函数代码没有编译到二进制文件中,而是位于 /usr/local/cpanel/Cpanel/Session.pm :

sub create {
    my (%args) = @_;
    my $session_name = _generate_session_name();
    my $ob = _generate_ob_secret();
    filter_sessiondata(\%args); # 过滤
    write_session($session_name, \%args);
    return "$session_name,$ob";
}

函数内会调用 filter_sessiondata 过滤 CRLF 。此时 write_session 写入文件到 /var/cpanel/sessions/raw 目录,文件内容如下:

Basic Auth 认证

cPanel/WHM 还支持 AUTH_HEADER 认证。如果携带 Cookie 访问非 /login 接口,将进入 handle_auth 函数完成 session 加载:

sub handle_auth {
    my $session = $server_obj->get_current_session;

    my $SESSION_ref;

    if ($session and ($session ne 'closed')) {    # session
        $session =~ tr[/][]d;    # 删除 / ,防止路径穿越

        $SESSION_ref = Cpanel::Session::Load::loadSession($session);

进入 loadSession ,将移除 cookie 中的 ob 后缀,后续会加载前面 badpass 调用 create 创建的文件,最终获取 session_ref 对象:

sub loadSession {
    my ($session) = @_;    # session

    my $ob = get_ob_part(\$session);

    my $cache_file = "/var/cpanel/sessions/cache/${session}.json";
    if (-f $cache_file) {
        my $json_data = Cpanel::AdminBin::Serializer::Load($cache_file);
        if ($json_data && ref $json_data eq 'HASH') {
            if ($ob && $json_data->{'pass'}) {
                my $encoder = Cpanel::Session::Encoder->new('secret' => $ob);
                $json_data->{'pass'} = $encoder->decode_data($json_data->{'pass'});
            }

            return $json_data;
        }
    }

    my $raw_file = "/var/cpanel/sessions/raw/${session}";
    ...
    # 加载解析
    return $session_ref;
}

回到 handle_auth 的 Basic Auth 解析环节,获取 header 中的 Authorization ,进行 base64 解码提取 user 和 pass :

my $auth_header = $server_obj->request->get_headers->{'authorization'};

    if (not($auth_header)) {
       ...
    }
    else {
        my($authtype, $encoded) = split(/ /u, $auth_header, 2);

        if ($authtype =~ /^Basic$/iu) {
            $server_obj->auth->set_auth_type('basic');

            my($user, $pass) = split(/:/u, decode_base64($encoded), 2);

            $user = $server_obj->auth->set_user($user);
            $pass = $server_obj->auth->set_pass($pass);

漏洞触发的关键点位于 set_pass 函数,该函数实现代码如下,没有对 \r\n 字符进行过滤检查:

# Cpanel::Server::Auth
sub set_user {
    my ($self, $user) = @_;
    $user =~ s{[\0/]}{}g;    # [1]
    $self->{'user'} = $user;
    return $user;
}

sub set_pass {
    my ($self, $pass) = @_;
    $pass =~ s/\0//g;         # [2]
    $self->{'pass'} = $pass;
    return $pass;
}

随后调用 _validate_username 验证用户名是否合法(root 合法)。在密码验证之前,请求数据会先保存到 session 中,在 handle_auth 返回前,这个数据写入操作已经完成。这里与前面 session create 的区别在于,没有调用 filter_sessiondata 进行 CRLF 过滤:

sub saveSession {
    my ($session, $session_ref, %options) = @_;

    my $ob = get_ob_part(\$session);

    return 0 if !is_valid_session_name($session);

    my $encoder = $ob && Cpanel::Session::Encoder->new('secret' => $ob);

    local $session_ref->{'pass'} = $encoder->encode_data($session_ref->{'pass'})
      if $encoder && length $session_ref->{'pass'};

    write_session($session, $session_ref);
}

CRLF 注入

注入的 \r\n 在 raw 文件中会被解析为独立的行。这是因为 Cpanel::Config::LoadConfig::loadConfig 在读取 session raw 文件时,会检测文件内容是否包含 \r ,如果包含则使用 \r?\n 作为行分隔符进行 split :

my $has_cr = ($cfg_txt =~ /\r/);
my $split_on = $has_cr ? '\r?\n' : '\n';

$conf_ref = {
    map { ($k, $v) = (split(m/=/, $_, 2))[0, 1]; defined($v) ? ($k, $v) : () }
    split(/$split_on/, $cfg_txt)
};

构造测试请求发现虽然返回 401 ,但是 CRLF 注入实际已经完成:

认证绕过到 RCE

上面的 CRLF 注入能用来干什么呢?handle_auth 函数返回后,后续在调用 docheckpass_whostmgrd 时,会通过 check_authok_user 函数进行用户验证,当存在 session_ref 对象中存在 successful_internal_auth_with_timestamp 键值,并且获取的时间戳在合理范围之内,会直接返回 AUTH_OK ,完全跳过后续的密码验证逻辑:

sub check_authok_user {
    my (%OPTS) = @_;
    my $user = $OPTS{'user'};

    my $session_ref = $server_obj->get_session_ref;

    if ($session_ref->{'successful_internal_auth_with_timestamp'}) { # [1]
        my $timestamp = $session_ref->{'successful_internal_auth_with_timestamp'};

        if ($timestamp > (time() - $SESSION_TIMEOUT)) {
            return $AUTH_OK;
        }
    }

    my $check_result = Cpanel::CheckPass::UNIX::checkpassword(
        $user, $OPTS{'pass'}, $OPTS{'encrypted_pass'}
    );
    return $check_result ? $AUTH_OK : $AUTH_FAILED;
}

很容易构造注入 successful_internal_auth_with_timestamp 的请求绕过认证并获取 ROOT 权限。

后续可结合一些 API 接口实现 RCE ,具体过程从略。

CVE-2026-41940 是一个多步骤的认证绕过漏洞,最终可导致匿名 RCE。漏洞触发的关键是set_pass没有进行CRLF过滤,同时saveSession也没有像create函数那样调用filter_sessiondata进行过滤,导致产生了CRLF注入。此外,关键的perl代码其实被编译进了cpsrvd,审计前需要进行动态提取,这种有难度的操作现在用AI 来辅助很容易实现。

由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用本人负责,公众号及文章作者不为此承担任何责任。

欢迎大家关注自在安全公众号。为更好学习交流,建了个技术交流群,大家可以扫描进群。你也可以关注公众号后@我拉你进群。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:自在安全 KCyber KCyber《CVE-2026-41940 深度分析影响7000万+量级的cPanel/WHM认证绕过漏洞:从CRLF注入到匿名RCE》

评论:0   参与:  0