了解我们在中国开展业务的承诺。 阅读全文

探索 VPN 设备:一位研究人员的探索之旅

Akamai Wave Blue

寫於

Ben Barnea

February 11, 2025

Akamai Wave Blue

寫於

Ben Barnea

Ben Barnea 是 Akamai 的安全研究人员,他专注于 Windows、Linux、物联网及移动设备等各种架构方面的低级别安全研究和漏洞研究并拥有丰富的经验。他喜欢了解复杂机制的工作原理,尤其是它们是如何失效的。

VPN 是接入企业网络的网关,因此这些设备中的漏洞会对企业产生重大影响。
VPN 是接入企业网络的网关,因此这些设备中的漏洞会对企业产生重大影响。
  • Akamai 研究员 Ben Barnea 发现 Fortinet FortiOS 中存在多个漏洞。
  • 未经身份验证的攻击者可触发可能导致 DoS 和 RCE 的漏洞
  • DoS 漏洞很容易被利用,并且会导致 FortiGate 设备无法正常运行。
  • 我们认为,RCE 漏洞难以被利用。
  • 已按披露责任将这些漏洞告知 Fortinet,其分配到的漏洞编号分别为 CVE-2024-46666CVE-2024-46668
  • Fortinet 已于 2025 年 1 月 14 日解决了 Barnea 发现的漏洞,安装了最新版本 FortiOS 的设备可免受这些漏洞的影响。

引言

过去几年,VPN 解决方案遭受多个严重漏洞,这些漏洞已被恶意攻击者广泛利用。其中一些漏洞极易被利用,并且会造成极具破坏性的影响,如连入互联网的 VPN 设备上的 RCE 漏洞。一旦进入网络,攻击者就可以横向移动,从而获取敏感数据、知识产权和其他高价值资产。

除了对 VPN 漏洞的初始利用外,Akamai 研究员 Ori David 还发现后利用技术,并表明遭入侵的 DNS 服务器可以让攻击者轻松控制网络中的其他关键资产。

遗憾的是,想要研究 VPN 设备的安全研究人员很难开始他们的研究,因为固件并不总是容易获得,而且受到供应商部署的加密机制的保护。但是,鉴于 VPN 设备是被利用的主要目标,对于攻击者而言,突破这些保护防线无疑是值得的。

在本博文中,我们将探讨关于 Fortinet VPN 解决方案的研究探索过程。我们将介绍获取固件、解密、设置调试程序以及最终查找漏洞的过程。

本文中介绍的一些研究并不新颖,OptistreamBishop FoxAssetnoteLexfo 等公司已经对 FortiOS 进行了一些高质量的研究。  由于 Fortinet 经常更改加密和解密方法,由此增加设备分析难度,我们使用最新版 FortiOS 对这些初步研究进行了更新。

获取固件映像

过去,VPN 作为单独的物理设备出售,这可能会使获取这些设备和提取固件变得困难。但如今,VPN 设备通常作为虚拟设备部署到虚拟机 (VM) 中。

幸运的是,Fortinet 提供试用版虚拟机,注册后可从其网站下载(图 1)。该虚拟机受到限制,仅允许使用一个 CPU,并且 RAM 限制为 2 GB。

Fortinet 提供试用版虚拟机,注册后可从其网站下载(图 1)。 图 1:可下载的试用版虚拟机

创建调试环境

所提供的虚拟机有两个关注焦点:(1) 启动映像和内核映像(即 flatkc);(2) 加密文件系统 rootfs,其中包含大多数值得关注的文件。解密该文件系统后,我们可以在 /bin/ 目录下找到一个名为 init 的二进制文件。

该虚拟机的大多数二进制文件都已静态编译到这个二进制文件中。/bin/init 中有两个值得关注的二进制文件,即 SSLVPND 和管理 Web 服务器。本文稍后将介绍这些二进制文件。

我们希望创建一个拥有完整 shell 的环境,而不是从 Fortinet 收到的受限 CLI。此外,我们还希望有一个 gdb 二进制文件,以便轻松进行二进制文件调试。

要创建这样的环境,我们需要执行以下操作:

  1. 解压缩经 GZIP 压缩的 CPIO(文件存档格式)
  2. 使用 Bishop Fox 的脚本解密 rootfs
  3. 解压缩 bin.tar.xz 存档
  4. 修补 /bin/init 完整性检查
  5. 使用 vmlinux-to-elf 将 flatkc 转换为 ELF
  6. 在 IDA 中找到 fgt_verify_initrd 的地址,以便在运行时环境中对其进行修补,以禁止进一步的完整性检查
  7. 将静态编译的 busybox 和 gdb 放入 /bin/ 中
  8. 编译用于创建 telnet 服务器的存根;使用此存根覆盖 /bin/smartctl
  9. 压缩 /bin/ 文件夹
  10. 重新打包 rootfs,对其进行加密
  11. 在加密的 rootfs 末尾添加填充
  12. 使用辅助 Ubuntu VM(挂载 VMDK)替换 VMDK 中的 rootfs

通过图 2 中所示的步骤,创建具有调试功能的已编辑虚拟机。由于 rootfs 的完整性检查失败,内核将停止执行。因此,我们需要修补内核 (flatkc),然后修补引导加载程序完整性验证代码,或者需要对内核完整性检查进行动态修补。我们决定采用后一种方法。

通过图 2 中所示的步骤,创建具有调试功能的已编辑虚拟机。 图 2:对 FortiGate 进行修补以用于研究环境

我们尝试通过编辑 VMDK 文件来使用 VMware 的虚拟机调试功能。这会设置一个 GDB 调试程序,一旦虚拟机运行,该调试程序就将可用。遗憾的是,在已启用 Hyper-V 的虚拟机上运行时,该功能遇到了与实现相关的问题。一旦遇到断点,虚拟机就会崩溃。这似乎是由于在 Hyper-V 虚拟机下运行时,内核调试功能的实现不完整造成的。

尝试使用 QEMU 创建正在运行的虚拟机

在多次尝试使用 VMware 运行内核调试失败后,我们决定尝试使用 QEMU 创建正在运行的虚拟机。我们使用类似的步骤静态创建了修改后的虚拟机,只是需要在 qcow2 和 VMDK 文件格式之间来回转换。

要在使用 QEMU 时调试内核,可以向 qemu-system 提供 -s 标志。最后,我们运行虚拟机、连接 GDB 并添加断点以覆盖完整性检查,然后就可以使用 CLI 了。(图 3)。

最后,我们运行虚拟机、连接 GDB 并添加断点以覆盖完整性检查,然后就可以使用 CLI 了。(图 3)。 图 3:可使用 CLI 的工作虚拟机

配置网络设置并从 DHCP 服务器接收有效 IP 后,我们就可以运行修改后的 smartctl 二进制文件,该二进制文件可打印当前目录内容和 Linux ID 命令并开启 busybox telnet 会话。(图 4)。

配置网络设置并从 DHCP 服务器接收有效 IP 后,我们就可以运行修改后的 smartctl 二进制文件,该二进制文件可打印当前目录内容和 Linux ID 命令并开启 busybox telnet 会话。(图 4)。 图 4:启用后门

最后,如图 5 所示,我们已连接到新创建的 telnet 服务器。

如图 5 所示,我们已连接到新创建的 telnet 服务器。 图 5:连接到 shell

是否已完成?否。 

正如图 6 所示,我们没有有效的许可证,无法与管理面板交互。

正如图 6 所示,我们没有有效的许可证,无法与管理面板交互。 图 6:许可证错误

无法(?)绕过

起初,我们认为许可证无效,因为我们超出了 1 个 CPU 和 2,048 MB RAM 的限制。那么,我们有两个选择:接受这一限制,让虚拟机运行得很慢;或者绕过这一限制。

经过一些逆向分析,我们发现了守护进程中定期调用的 upd_vm_check_license 函数(图 7)。该函数会检查虚拟机的 RAM 大小和 CPU 数量是否未超出限制。

经过一些逆向分析,我们发现了守护进程中定期调用的 upd_vm_check_license 函数(图 7)。 图 7:造成虚拟机约束的反编译代码

通过动态修改 num_max_CPUs() max_allowed_RAM() 的返回值绕过限制后,我们现在拥有一个功能强大、不受限制的虚拟机,启动虚拟机时收到的错误也减少了,但我们仍然收到许可证无效的错误。

我们花了大量时间对许可证验证函数进行逆向分析,思考哪些选择导致我们出现这种情况,最终发现许可证使用的是虚拟机的串行密钥,该密钥使用 SMBIOS UUID 生成。由于我们没有为 QEMU 提供该密钥,因此它使用了一个 NULL 密钥,这就导致创建的序列号为“FGVMEV0000000000”。使用以下标志向 QEMU 提供 SMBIOS UUID 后:

  -smbios type=1,manufacturer=t1manufacturer,product=t1product,version=t1version,serial=t1serial,
  uuid=25359cc8-5fe7-4d50-ab82-9fd15ecaf221,sku=t1sku,family=t1family

我们终于启动了虚拟机并获得了有效许可证。 

新版本。新加密。为什么?

至此,我们有了一个可以正常工作的调试环境。我们开始研究管理 Web 服务器,并发现了一些漏洞,我们将在本文稍后部分中进行描述。在发现这些漏洞的同时,我们注意到 Fortinet 发布了 FortiOS 7.4.4 版。

我们想看看新版本中是否仍然存在这些漏洞,但在解密更新后的虚拟机 rootfs 失败后,我们遇到了完全不同的加密方式,这种新加密破解起来更加困难。这一次,我们决定努力解密新的 rootfs,而不是创建调试环境,因为此时的主要目标是验证漏洞是否仍然存在。

首先,我们来介绍一下解密 rootfs 的旧方法(7.4.4 版之前的版本):

1.内核会验证 rootfs 完整性,如果有效,则继续下一步

2.内核调用 fgt_verifier_key_iv,按如下方式计算密钥和 IV:

a. 密钥:全局数据的 sha256()

b. IV:同一全局数据的另一部分的 sha256();然后,将结果截断为 16 个字节

3.使用 Chacha20 以及上述密钥和 IV 解密 rootfs

现在,让我们来看看新算法(图 8):

1.与之前的算法一样,解密代码通过全局数据缓冲区的 sha256() 计算密钥和 IV

2.ChaCha 使用密钥和 IV 解密内存块;此内存块是代表 RSA 私钥的 ASN1

3.从 RSA 私钥中获取 d,n,并使用已知公式 M = Cd mod N 解密加密固件最后 256 个字节中存在的数据块

4.从数据块中获取:

  • 16 个字节,即 nonce + counter 

  • 32 个密钥字节  

5.使用密钥和 nonce+counter 在 CTR 模式下进行 AES 解密;代码使用自定义 CTR 加法

6.该加法是对 (nonce + counter) 的半字节进行异或 (XOR) 运算的结果

 

新算法(图 8): 图 8:固件解密流程图

为了增加这种多级解密算法的复杂性,新的 flatkc 不再使用符号,因此编写自动解密固件的工具变得更加困难,例如,查找用于 ChaCha 解密的全局数据和加密的 RSA 私钥。

完成上述所有步骤后,我们即可查看 rootfs(图 9)。

完成上述所有步骤后,我们即可查看 rootfs(图 9)。 图 9:rootfs 的 tar 包

这次,我们没有创建修改后的环境。这样做需要创建一个 rootfs 存档,而该存档需要按如上所述方式成功解密。另一种方法是动态设置断点,并使用解密后的 rootfs 覆盖内存。

对管理 Web 服务器进行逆向分析

我们终于可以对管理 Web 服务器进行逆向分析了。该服务器基于 Apache,一般来说,不应通过互联网访问(与 sslvpn 界面相反,后者可通过互联网访问)。

打开 httpd 配置,我们会发现一些位置指令将 URL 指向其处理程序(图 10)。

打开 httpd 配置,我们会发现一些位置指令将 URL 指向其处理程序(图 10)。 图 10:httpd 配置文件的片段

然后,我们可以在二进制文件中搜索某个处理程序字符串,以找到相应的处理程序表(图 11)。

然后,我们可以在二进制文件中搜索某个处理程序字符串,以找到相应的处理程序表(图 11)。 图 11:IDA 中所示的处理程序列表

由于我们的目标是查找未经验证的漏洞,因此决定专注于可通过 /api/v2/authentication URL 访问的 api_authentication-handler

在深入逆向分析工作之前,建议先在 IDA 中创建一些 Apache 结构和连接结构以减轻工作量(图 12 和 13)。

  struct __attribute__((aligned(8))) _request_rec
{
    apr_pool_t *pool;
    conn_rec *connection;
    void *server;
    _request_rec *next;
    _request_rec *prev;
    _request_rec *main;
    char *the_request;
    int assbackwards;
    int proxyreq;
    int header_only;
    int proto_num;
    char *protocol;
    const char *hostname;
    unsigned __int64 request_time;
    const char *status_line;
    int status;
    enum http_methods method_number;
    const char *method;
    unsigned __int64 allowed;
    void *allowed_xmethods;
    void *allowed_methods;
    unsigned __int64 sent_bodyct;
    unsigned __int64 bytes_sent;
    unsigned __int64 mtime;
    const char *range;
    unsigned __int64 clength;
    int chunked;
    int read_body;
    int read_chunked;
    unsigned int expecting_100;
    void *kept_body;
    void *body_table;
    unsigned __int64 remaining;
    unsigned __int64 read_length;
    void *headers_in;
    void *headers_out;
    void *err_headers_out;
    void *subprocess_env;
    void *notes;
    const char *content_type;
    const char *handler;
    const char *content_encoding;
    void *content_languages;
    char *vlist_validator;
    char *user;
    char *ap_auth_type;
    char *unparsed_uri;
    char *uri;
    char *filename;
    char *canonical_filename;
    char *path_info;
    char *args;
    int used_path_info;
    int eos_sent;
    void *per_dir_config;
    void *request_config;
    void *log;
    const char *log_id;
    void *htaccess;
    void *output_filters;
    void *input_filters;
    void *proto_output_filters;
    void *proto_input_filters;
    int no_cache;
    int no_local_copy;
    void *invoke_mtx;
    apr_uri_t parsed_uri;
    apr_finfo_t finfo;
    void *useragent_addr;
    char *useragent_ip;
    void *trailers_in;
    void *trailers_out;
    char *useragent_host;
    int double_reverse;
    unsigned __int64 bnotes;
};

图 12:Apache 结构

  struct __attribute__((aligned(8))) conn_rec
{
    apr_pool_t *pool;
    void *base_server;
    void *vhost_lookup_data;
    apr_sockaddr_t *local_addr;
    sockaddr *client_addr;
    char *client_ip;
    char *remote_host;
    char *remote_logname;
    char *local_ip;
    char *local_host;
    __int64 id;
    void *conn_config;
    void *notes;
    void *input_filters;
    void *output_filters;
    void *sbh;
    void *bucket_alloc;
    void *cs;
    int data_in_input_filters;
    int data_in_output_filters;
    unsigned __int32 clogging_input_filters : 1;
    __int32 double_reverse : 2;
    unsigned int aborted;
    ap_conn_keepalive_e keepalive;
    int keepalives;
    void *log;
    const char *log_id;
    conn_rec *master;
    int outgoing;
};

图 13:连接结构

在逆向分析身份验证处理程序时,我们首先对名为 api_login_handler 的 POST 方法处理程序进行了逆向分析。该函数通过调用 api_login_parse_param 从请求中检索登录参数。该函数会尝试根据 content-type 标头解析 POST 数据:

  1. 如果设置为“multipart/form-data”,则请求包含 HTML 表单

  2. 如果未进行此设置,则会读取纯 POST 数据

第二种情况非常简单,因此我们主要关注第一种情况。通过浏览反编译代码,我们很快注意到一个指向 libapreq 库的调试字符串(图 14)。

通过浏览反编译代码,我们很快注意到一个指向 libapreq 库的调试字符串(图 14)。 图 14:表示代码使用 libapreq 的字符串

由于 libapreq 是一个开源 Apache 库,我们(几乎)没有理由在反编译代码中而不是源代码中寻找漏洞。因此,我们需要做的第一件事就是找到库版本。经过一些来回尝试,我们找到了二进制文件和特定提交中存在的一个字符串,但该字符串在一次提交后被删除,从而成功缩小了版本范围(图 15)。

我们需要做的第一件事就是找到库版本。经过一些来回尝试,我们找到了二进制文件和特定提交中存在的一个字符串,但该字符串在一次提交后被删除,从而成功缩小了版本范围(图 15)。 图 15:比较二进制反编译代码与删除字符串的源代码提交

令人惊讶的是,二进制文件中存在的库是在 2000 年 3 月发布的最低版本(图 16)。

令人惊讶的是,二进制文件中存在的库是在 2000 年 3 月发布的最低版本(图 16)。 图 16:缩小后的 libapreq 版本范围

漏洞

Fortinet 使用该模块的方式与 25 年前几乎一模一样,只是出于优化目的做了一些非常细微的更改。当第一次发现这一点时,我们认为 2000 年的代码不可能没有漏洞。结果我们是对的!

在了解漏洞之前,让我们先解释一下该库的目的和用途。Apreq 是一个 Apache 库,用于处理客户端请求数据。从用户处接收数据的一种常见方式是 HTML 表单。可使用不同的编码方法将填写的表单数据传递给服务器,但常用的方式是 application/x-www-form-urlencodedmultipart/form-data

使用 multipart/form-data 时,客户端(通常是浏览器)会选择任意文本作为表单数据不同字段之间的边界。边界通过 HTTP 标头指定。边界还可用来指示表单数据的结束(图 17)。

  POST /foo HTTP/1.1
  Content-Length: 68137
  Content-Type: multipart/form-data; boundary=ExampleBoundaryString

  --ExampleBoundaryString
  Content-Disposition: form-data; name="description"

  Description input value
  --ExampleBoundaryString
  Content-Disposition: form-data; name="myFile"; filename="foo.txt"
  Content-Type: text/plain

  [content of the file foo.txt chosen by the user]
  --ExampleBoundaryString--

图 17:HTTP 边界表单示例(来源

现在,让我们来看看发现的一些漏洞,包括 NULL 字节越界 (OOB) 写入、wild copy、设备 DoS、Web 服务器 DoS 和 OOB 读取。

NULL 字节越界写入

multipart_buffer_read 填充内部缓冲区并查找边界后,它会返回当前位置和找到的边界之间的字符串。漏洞在于,如果边界不在内部缓冲区的开头,则会在删除最后两个本该是行结尾的字符(“\r\n”)后返回字符串。代码错误地假设返回的字符串长度大于 2

在图 18 中,retval 是返回的字符串,start 是其长度,等于 1。在这种情况下,blen 也等于 start。然后,将其减 2,得到值 -1。这样,我们就能在缓冲区前一个字节写入 NULL

在图 18 中,retval 是返回的字符串,start 是其长度,等于 1。在这种情况下,blen 也等于 start。然后,将其减 2,得到值 -1。这样,我们就能在缓冲区前一个字节写入 NULL。 图 18:导致缓冲区前出现 NULL 越界写入漏洞

漏洞利用尝试

虽然一个字节溢出(甚至一个位溢出)就足以实现代码执行,但我们认为此漏洞在实际情况下不太可能被利用。首先,我们只能写入一个 NULL 字节,而且只能在缓冲区前写入一个字节。缓冲区在堆上分配,因此会有两种情况:

1.缓冲区是要在堆节点上分配的第一个缓冲区。在这种情况下,我们将在分配之前获得堆节点元数据(图 19)。

缓冲区是要在堆节点上分配的第一个缓冲区。在这种情况下,我们将在分配之前获得堆节点元数据(图 19)。 图 19:代表 Apache 内存堆节点的结构

我们将覆盖 endp 指针的一个字节。这不会影响指针的值,因为我们会根据字节序覆盖指针的最高字节。由于虚拟机是 x64,因此该字节将始终为 0。

2.如果在我们之前已有分配,就会覆盖一个字节的数据。遗憾的是,与前面的示例一样,在大多数情况下,结构的末尾有一个指针、填充,或者一个已经以 NULL 结束的 C 字符串。

我们确实发现了一个值得关注的对象:multipart_buffer C 结构(图 20)。

我们确实发现了一个值得关注的对象:multipart_buffer C 结构(图 20)。 图 20:multipart_buffer C 结构

在这种情况下,我们认为可以通过上一个漏洞使结构中的最后一个变量 buffer_len 为负值,然后使用此漏洞将其从负值更改为较大的正值(通过覆盖 MSB 字节将该数值标记为负值)。

虽然这种方法似乎很吸引人,但存在两个问题:

1.该结构只创建一次,即在创建表单解析器时创建。这意味着我们无法轻松扩散此对象。

2.一旦使用了上一个漏洞,就限制了填充函数中的读取长度。这意味着一旦填充函数结束,在读取完整请求的 POST 数据后,self->length 为 0。下次代码调用 multipart_buffer_read 时,它将找不到高级缓冲区的边界(因为我们使结束指针出现在其开始之前),并且由于 self->length 为 0,它将退出并发出上传格式错误警报。

我们曾考虑尝试在争用条件下利用此漏洞,但在检查 Apache 多进程处理模式 (MPM) 时,我们看到了图 21 中所示情况。

我们曾考虑尝试在争用条件下利用此漏洞,但在检查 Apache 多进程处理模式 (MPM) 时,我们看到了图 21 中所示情况。 图 21:检查 Apache MPM 模式

这意味着 Apache 将为多个进程创建分支,每个进程处理一个请求。我们还可以注意到,这些进程不是多线程。这意味着我们将无法在争用条件下利用此漏洞。

wild copy

在同一个函数 multipart_buffer_read 中,如果代码找不到边界(start 等于 -1),则只会返回内部缓冲区的一部分:(bytes - boundary_length)。此处的错误在于,bytes 被设置为常量值 5120,而边界长度可能大得多(达到标头长度的限制)。

因此,如果发送一个边界不在第一个区块中的字段,并且边界长度大于 5120,就会导致 blen 为负值。这样,代码就会将 self->buffer 设置为缓冲区之前,并将 self->buffer_len 设置为更大的值(图 22)。

这样,代码就会将 self->buffer 设置为缓冲区之前,并将 self->buffer_len 设置为更大的值(图 22)。 图 22:整数下溢导致的漏洞

漏洞利用尝试

此漏洞与上一个漏洞有所不同,这次由于 start 为负值(找不到边界),我们无法找到写入 NULL 字节的代码

blen 是 multipart_buffer_read 函数的一个参数,我们来观察一下调用该函数并接收 blen 作为输出的 multipart_buffer_read_body 函数。

如图 23 所示,blen 被使用了两次:

1.如果这是创建的第一个缓冲区,则会使用 blen 复制从 multipart_buffer_read 接收的字符串。在这种情况下,Apache 会引发内存不足 (OOM) 错误,代码会中止。这可用于发起 DoS 攻击。

2.如果这是第二个区块,则会使用 my_join 函数将上一个区块和当前区块连接起来。该函数调用值为负数的 memcpy,从而导致 wild copy。

如图 23 所示,blen 被使用了两次 图 23:multipart_buffer_read_body 中的两种不同流程

(有些人可能已经注意到此代码中的另一个漏洞,即 old_len 被更新为 blen 而不是 old_len + blen,这会导致用户数据被截断。)

即使有可能利用这种 wild copy 漏洞,但也非常困难。首先,我们没有任何可“停止”wild copy 的选择,它是一个较大的 memcpy。其次,没有多线程,因此我们无法覆盖将在另一个线程中同时使用的对象。我们认为,唯一可能的选择是利用信号处理程序(如果它不执行安全退出)。

设备 DoS

此漏洞实际上并不存在于库本身,而是存在于使用该库的 Fortinet 代码中。

当用户通过表单上传文件(该表单在 POST 数据中由 Content-Disposition 标头指定)(见图 17)时,会在 /tmp/ 文件夹中创建一个新文件,文件名为 /tmp/uploadXXXXXX,其中 X 是随机字符。

每次上传文件时,都会创建一个适当的结构,并将其插入到已上传文件的链接列表中。如果仅删除链接列表第一个节点中的文件,就会在解析结束时出现此漏洞。这会让攻击者能够通过填充 /tmp/ 文件夹来发起攻击。由于 /tmp/ 是 tmpfs 文件系统,因此数据存储在 RAM 上。这会导致整个系统出现 OOM 的情况,从而导致设备卡住。

只有重启设备才能使其恢复正常使用,但即使这样也无法保证。在一次尝试中,我们给设备造成了某种网络“变砖”:即使重启设备后,网络功能也无法正常工作,我们无法使用或连接设备。

漏洞利用尝试

此漏洞很容易被利用。只需要使用包含多个文件的表单重复发送请求即可。不久之后,设备就会卡住(图 24)。

只需要使用包含多个文件的表单重复发送请求即可。不久之后,设备就会卡住(图 24)。 图 24:用未删除的文件填充 VPN 设备的 RAM,最终因内存不足而导致 DoS

Web 服务器 DoS

这是 multipart_buffer_headers 函数中的一个小漏洞。它调用 multipart_buffer_fill,填充内部缓冲区,然后在内部缓冲区查找双行结尾。

漏洞在于,调用 multipart_buffer_fill 后,代码不会检查内部缓冲区是否有效。如果客户端在 multipart_buffer_fill 等待输入时断开连接,就会将缓冲区设置为 NULL,这会导致 NULL 取消引用(图 25)。

如果客户端在 multipart_buffer_fill 等待输入时断开连接,就会将缓冲区设置为 NULL,这会导致 NULL 取消引用(图 25)。 图 25:访问内部缓冲区,而不检查其是否有效

漏洞利用尝试

此漏洞也很容易被利用。攻击者可以创建多个线程,从而创建连接并发送崩溃请求。由于 Apache pre-fork MPM 性能不佳,因此在遭受攻击期间,服务器将无法处理多次崩溃,也无法为其他客户端提供服务。

OOB 读取

该库使用定期填充的内部缓冲区。填充缓冲区时,将按如下过程计算要读取的字节数:

1.计算 (bytes_requested - current_internal_buffer_len)

2.加上边界长度 + 2(用于“\r\n\r\n”)

3.计算此结果与要从 POST 数据中读取的可用字节数之间的最小值

此漏洞存在于 multipart_buffer_read 中。如果在内部缓冲区的开头找到边界,并且不是标记表单结尾的边界,则会将内部缓冲区指针推进到边界之外,并为行结尾添加另外 2 个字节

此处的错误在于,攻击者可以使内部缓冲区正好等于边界的大小,从而导致代码超出内部缓冲区的末尾,向前移动两个字节(图 26)。之所以可以做到这一点,是因为我们可以通过提供比 bounding_length + bytes_requested 更少的字节数来“限制”内部缓冲区计算。

此处的错误在于,攻击者可以使内部缓冲区正好等于边界的大小,从而导致代码超出内部缓冲区的末尾,向前移动两个字节(图 26)。 图 26:让内部缓冲区超出其末尾

漏洞利用尝试

如图 26 所示,代码返回 NULL。然而,正如我们在上一个漏洞中所看到的,multipart_buffer_headers 中的代码直接访问内部缓冲区,而不是返回值。这将导致代码在缓冲区后查找“\r\n”,并将其作为标头返回。

在登录处理程序中使用 apreq 时,代码会读取表单字段,但不会将其发送回客户端,因此在这种情况下,我们无法将 OOB 漏洞用作信息泄露手段。该库也在 Web 服务器中多次使用,但似乎只有经过身份验证的用户才能使用,因此具有低权限用户凭据的攻击者最多只能通过从内存中读取高权限用户凭据以将其用作权限升级漏洞。

SSLVPND

SSLVPND 是负责处理 Fortinet SSL-VPN 组件的守护进程。它可以访问互联网。SSLVPND 也使用 apreq 库。这里所用的库基于相同的旧版 apreq,但做了一些修改。除设备 DoS 漏洞外,上述所有其他漏洞也存在于 SSLVPND 中。

遗憾的是,我们无法触发 SSLVPND 中的 apreq 库,因此我们无法确认未经身份验证的用户是否可以利用这些漏洞,或者这些漏洞是否可能在 SSLVPND 上下文中被利用。

总结

由于 VPN 可以通过互联网访问,因此攻击者经常将其作为攻击目标。在本博文中,我们举例说明了研究 VPN 设备的方法。虽然没有发现任何严重漏洞,但我们认为肯定还有其他漏洞有待发现。

VPN 是接入企业网络的网关,因此这些设备中的漏洞会对企业产生重大影响。我们希望本博文也能鼓励更多安全研究人员寻找 VPN 漏洞。



Akamai Wave Blue

寫於

Ben Barnea

February 11, 2025

Akamai Wave Blue

寫於

Ben Barnea

Ben Barnea 是 Akamai 的安全研究人员,他专注于 Windows、Linux、物联网及移动设备等各种架构方面的低级别安全研究和漏洞研究并拥有丰富的经验。他喜欢了解复杂机制的工作原理,尤其是它们是如何失效的。