Fortinet FortiGate CVE-2024-23113 – A Super Complex Vulnerability In A Super Secure Appliance In 2024
Today we’d like to share a recent journey into (yet another) SSLVPN appliance vulnerability – a Format String vulnerability, unusually, in Fortinet’s FortiGate devices.
今天,我们想分享最近关于(又一个)SSLVPN 设备漏洞的旅程 – Fortinet 的 FortiGate 设备中罕见的格式字符串漏洞。
It affected (before patching) all currently-maintained branches, and recently was highlighted by CISA as being exploited-in-the-wild.
它影响了(在修补之前)所有当前维护的分支,最近被 CISA 强调为在野外被利用。
This must be the first time real-world attackers have reversed a patch, and reproduced a vulnerability, before some dastardly researchers released a detection artefact generator tool of their own. /s
这一定是现实世界的攻击者第一次撤销补丁并重现漏洞,然后一些卑鄙的研究人员发布了他们自己的检测伪影生成器工具。/秒
At watchTowr’s core, we’re all about identifying and validating ways into organisations – sometimes through vulnerabilities in network border appliances – without requiring such luxuries as credentials or asset lists.
在 watchTowr 的核心,我们致力于识别和验证进入组织的方式 – 有时通过网络边界设备中的漏洞 – 而无需凭证或资产列表等奢侈品。
While full exploitation is sometimes required to make our point – our clients rely on watchTowr technology to rapidly tell them, within hours, if they’re affected with 100% precision. When you don’t have the luxury of time that would allow us to create a stable, 100% reliable PoC – ultra-reliable detection of a vulnerable system is just as good.
虽然有时需要充分利用才能证明我们的观点 – 但我们的客户依靠 watchTowr 技术在数小时内以 100% 的精度快速告诉他们是否受到影响。当您没有充足的时间让我们创建稳定、100% 可靠的 PoC 时,对易受攻击的系统进行超可靠检测同样出色。
Occasionally, while reproducing a vulnerability, we find the story behind the vulnerability to be much more interesting than just the usual “version X is vulnerable and version Y is patched”.
有时,在重现漏洞时,我们会发现漏洞背后的故事比通常的“版本 X 易受攻击,版本 Y 已修补”要有趣得多。
This analysis is a good example of such, and thus we decided to release it given the relevance of this vulnerability – we set out to prove the exploitability of Fortinet FortiGate’s CVE-2024-23113, and ended up down a bigger rabbit hole than we thought.
此分析就是一个很好的示例,因此鉴于此漏洞的相关性,我们决定发布它 – 我们着手证明 Fortinet FortiGate 的 CVE-2024-23113 的可利用性,结果陷入了比我们想象的更大的兔子洞。
Tl;dr SSLVPN appliances are still sUpEr sEcurE.
tl;dr SSLVPN 设备仍然是 sUpEr sEcurE。
Please note: Sometimes people tweet at us to tell us that we analysed and wrote about a vulnerability that we didn’t find ourselves. Yes, this is what we do when we analyse Ndays. To avoid confusion/notifications, this is also not our vulnerability – this vulnerability is attributed to “Gwendal Guégniaud of [the] Fortinet Product Security team”. Stop tweeting at us.
请注意:有时人们会发推文告诉我们,我们分析并撰写了一个我们自己没有发现的漏洞。是的,这就是我们在分析 Ndays 时所做的。为避免混淆/通知,这也不是我们的漏洞 – 此漏洞归因于“Fortinet 产品安全团队的 Gwendal Guégniaud”。停止向我们发推文。
The Vulnerability 漏洞
First up, a little about the vulnerability.
首先,我们来了解一下漏洞。
It’s a Format String vulnerability, which honestly are an uncommon breed these days, since they are typically trivial to find via static analysis. They come about when a developer/programmer/LLM allows a ‘format string’ to be controlled by an attacker.
这是一个格式字符串漏洞,老实说,现在这种情况并不常见,因为它们通常很容易通过静态分析找到。当开发人员/程序员/LLM 允许攻击者控制“格式字符串”时,就会出现这种情况。
As always, less words more show, an example is best – consider the following, which takes a string passed by an attacker:
与往常一样,字数越少,最好举个例子 – 考虑以下内容,它接受攻击者传递的字符串:
void doStuff(char* stuffToDo)
{
printf("%s", stuffToDo);
}
In this code, the printf
invocation is correct – no Format String vulnerability here.
在此代码中,printf
调用是正确的 – 此处没有格式字符串漏洞。
If, however, the programmer invokes slightly differently:
但是,如果程序员的调用略有不同:
void doStuff(char* stuffToDo)
{
printf(stuffToDo);
}
In this case – what if the string that the user passes contains the magical “%s
”? We, now, have a Format String vulnerability.
在这种情况下 – 如果用户传递的字符串包含神奇的 “%s
” 怎么办?我们现在有一个 Format String 漏洞。
The printf
function will try to print a string, reading it from the stack, even though none has been passed. This situation quickly leads to Remote Code Execution via one of many well-studied mechanisms, which we won’t reproduce here.
printf
函数将尝试打印字符串,从堆栈中读取它,即使没有传递任何内容。这种情况很快就会通过许多经过充分研究的机制之一导致远程代码执行,我们不会在这里重现。
Suffice to say, letting an attacker control your format strings leads to RCE.
可以说,让攻击者控制您的格式字符串会导致 RCE。
As you’d expect from the SSLVPN world, this is roughly (or perhaps exactly) what Fortinet did.
正如您对 SSLVPN 世界所期望的那样,这大致(或可能完全)是 Fortinet 所做的。
As we say, this class of vulnerability is something of a rarity outside of CTFs, since it is so easy to find at compile-time via static analysis. It’s rare that there’s a legitimate case for a format string to be supplied at run-time at all, and when they are, they are almost always read-only.
正如我们所说,这类漏洞在 CTF 之外是很少见的,因为它很容易在编译时通过静态分析找到。在运行时提供格式字符串的合法情况很少见,即使提供,它们几乎总是只读的。
It’s worth noting that the credit on this vulnerability goes to “Gwendal Guégniaud of [the] Fortinet Product Security team”, which suggests this is exactly what Fortinet did – ran a sweep for Format String vulnerabilities, and found one. It’s worth doing exactly this in our own (and other people’s!) codebases from time to time.
值得注意的是,这个漏洞的功劳归功于“Fortinet 产品安全团队的 Gwendal Guégniaud”,这表明这正是 Fortinet 所做的 – 对格式字符串漏洞进行了扫描,并发现了一个。在我们自己(和其他人)的代码库中不时地这样做是值得的。
The advisory states that, to mitigate, administrators should prevent access to the FGFM service. This is a good place to start on our search – what exactly is the FGFM service, and how can we break it?
该建议指出,为了缓解这种情况,管理员应阻止对 FGFM 服务的访问。这是我们搜索的好地方 – FGFM 服务到底是什么,我们如何打破它?
Not-So High availability 不那么高可用性
Some Googling later, we find that ‘FGFM’ stands for ‘FortiGate to FortiManager [protocol]’, and is used for central administration of FortiGate devices by a FortiManager console.
后来在谷歌上搜索一下,我们发现“FGFM”代表“FortiGate 到 FortiManager [协议]”,用于 FortiManager 控制台对 FortiGate 设备的集中管理。
Rather handily, Fortinet published a protocol guide documenting some high-level details of the protocol itself, which reveals some information that’ll help us get started – it runs over an SSL connection established over TCP port 541, and it performs the following duties:
相当容易的是,Fortinet 发布了一份协议指南,记录了协议本身的一些高级细节,其中揭示了一些有助于我们入门的信息 – 它通过通过 TCP 端口 541 建立的 SSL 连接运行,并执行以下职责:
The fgfm protocol implements a secure communication protocol with the following functions:
1. FortiGate reachability status (from FortiManager)
2. FortiManager reachability status (from FortiGate)
3. Configuration installation and retrieval
4. Script push
5. JSON monitoring via RTM
It seems that this protocol is also used for high-availability failover, allowing a secondary ‘standby’ device to swoop in and take over the duties of a failed device.
该协议似乎也用于高可用性故障转移,允许辅助 “备用” 设备突然进入并接管故障设备的职责。
This is interesting background. Now that we have the basics down, though, it’s time for action – let’s patchdiff between a vulnerable and a patched FortiGate instance and see if we can locate the vulnerability.
这是一个有趣的背景。不过,现在我们已经了解了基础知识,是时候采取行动了 – 让我们在易受攻击的 FortiGate 实例和修补的 FortiGate 实例之间修补差异,看看我们是否可以找到漏洞。
When patchdiffing, things are always easiest if we can find two versions that have minimal changes aside the change we’re interested in (the vulnerability fix itself).
当 patchdiff 时,如果我们能找到两个版本,除了我们感兴趣的更改(漏洞修复本身)之外,它们的变化最小,事情总是最简单的。
We are aided in this by the fact that Fortinet maintain not one, but three different branches of their firmware – the newest version, 7.4, the slightly-older 7.2, and the much-older 7.0. We can look at the patch notes for the fixed version, and pick out the version which has the least amount of fixes released.
Fortinet 维护其固件的不是一个,而是三个不同的分支,这一事实对我们有所帮助 – 最新版本 7.4、稍旧的 7.2 和更旧的 7.0。我们可以查看修复版本的补丁说明,并挑选出发布的修复数量最少的版本。
This turns out to be the 7.2 branch. We’ll be diffing between 7.2.6
and 7.2.7
.
结果是 7.2 分支。我们将在 7.2.6
和 7.2.7
之间进行差异。
Diffing Time! 时差!
Patchdiffing the FortiGate is always a time-consuming process, as all logic is inside a 70MB init
binary, and must be patchdiffed as a whole.
对 FortiGate 进行 Patchdiff 始终是一个耗时的过程,因为所有 logic 都在 70MB 的 init
二进制文件中,并且必须作为一个整体进行 patchdiff。
We ran the industry-standard Diaphora over the two versions, and were greeted by an not-unexpectedly huge amount of changes.
我们在两个版本上运行了行业标准的 Diaphora,并收到了不出所料的巨大变化。
Zooming in on those related to the FGFM service – identifiable by the some variant of the string ‘fgfm’ being referenced – we quickly found a likely culprit:
放大与 FGFM 服务相关的内容 – 通过引用的字符串 ‘fgfm’ 的某个变体来识别 – 我们很快找到了一个可能的罪魁祸首:
On the left, we have the patched version, and on the right, the vulnerable. We can see here that the newer version of the code contains a hardcoded format string of %s
, while the older version passes a variable directly.
左侧是修补版本,右侧是易受攻击的版本。我们可以看到,新版本的代码包含一个硬编码的格式字符串 %s
,而旧版本直接传递一个变量。
This looks like what we’re looking for!
这看起来就是我们要找的!
A little bit of reversing shows us that this suspect function is responsible for parsing some of the FGFM message. We can get a good idea of the protocol just by reading the code – it is an ASCII-based newline-delimited format, terminated with newlines, accepting ASCII key/value pairs delimited by an ‘=’ symbol.
稍微反转一下,我们就会发现这个可疑函数负责解析一些 FGFM 消息。我们只需阅读代码就可以很好地了解该协议 – 它是一种基于 ASCII 的换行符分隔格式,以换行符结尾,接受由 ‘=’ 符号分隔的 ASCII 键/值对。
Time to attach a debugger and started experimenting with the protocol itself.
是时候附加调试器并开始试验协议本身了。
Reversing The FGFM Protocol
反转 FGFM 协议
According to the documentation, this ASCII transfer occurs over TLS, and so we’ll need a TLS client to tunnel our experimentations over.
根据文档,这种 ASCII 传输是通过 TLS 进行的,因此我们需要一个 TLS 客户端来通过隧道进行实验。
We pulled out the ubiquitous openssl
client, invoking it with s_client
, and tried to connect to a vulnerable instance:
我们提取了无处不在的 openssl
客户端,使用 s_client
调用它,并尝试连接到一个易受攻击的实例:
$ openssl s_client -port 541 -host 192.168.70.19 -quiet
100000000A000000:error:0A0000F4:SSL routines:ossl_statem_client_read_transition:unexpected message:ssl/statem/statem_clnt.c:398:
Huh? 哼?
What’s going on here, why has the FortiGate device refused our SSL connection? A packet capture shows us that the TCP connection is established without problem, and that the server sends a ‘client hello’, which we respond to, just prior to the connection being terminated:
这是怎么回事,为什么 FortiGate 设备拒绝我们的 SSL 连接?数据包捕获向我们显示 TCP 连接已建立,没有问题,并且服务器在连接终止之前发送了“客户端你好”,我们对此进行了响应:
.. Wait, hang on, let’s read that again. The server sends a client hello?
..等等,等等,让我们再读一遍。服务器向客户端发送你好?
Those intimately familiar with the TLS handshake may be tilting their heads in curiosity at this point.
此时,那些熟悉 TLS 握手的人可能会好奇地歪着头。
As they are well aware, the usual handshake process is for the server end to send a server hello, and the client to send a client hello. Here, however, the FortiGate device has sent a client hello, despite acting as a server, in TCP terms!
正如他们所清楚的那样,通常的握手过程是服务器端向服务器发送你好,客户端向客户端发送你好。然而,在这里,FortiGate 设备已经向客户端发送了你好,尽管它充当服务器,但以 TCP 术语表示!
Perhaps, then, it is expecting a server hello, rather than a client hello, even though it is our client which initiates the connection? Let’s give it a try.
那么,也许它期待的是服务器你好,而不是客户端你好,即使发起连接的是我们的客户端?让我们试一试。
The easiest way we found for openssl
to send a server hello is to run it in server mode (ie, with s_server
as opposed to s_client
), and then bridge the TCP connection using socat
.
我们发现 openssl
发送服务器你好的最简单方法是在服务器模式下运行它(即使用 s_server
而不是 s_client
),然后使用 socat
桥接 TCP 连接。
This way, our ‘client’ listens for a new connection, which is initiated by socat
, which connects the other end of the conversation to the server socket exposed by the FortiGate.
这样,我们的“客户端”会监听由 socat
发起的新连接,该连接将对话的另一端连接到 FortiGate 公开的服务器套接字。
We did it like this:
我们是这样做的:
openssl s_server -port 123 --key key.pem --cert cert.pem --ign_eof -ignore_unexpected_eof -quiet &
socat TCP-CONNECT:127.0.0.1:123 TCP-CONNECT:<FortiGate IP>:541
Since we’re running as a server, we’ll need to generate a TLS key – just use openssl for this, and generate a self-signed certificate (we’ll talk more about this certificate later). For now, though, we can finally get some life from the FortiGate:
由于我们作为服务器运行,因此需要生成一个 TLS 密钥 – 只需使用 openssl 并生成一个自签名证书(我们稍后将详细讨论此证书)。不过,现在,我们终于可以从 FortiGate 中获得一些生命:
$ openssl s_server -port 123 --key key.pem --cert cert.pem --ign_eof -ignore_unexpected_eof -quiet &
[1] 1518
$ socat TCP-CONNECT:127.0.0.1:123 TCP-CONNECT:<FortiGate IP>:541
6▒Nget auth
fg_ip=192.168.70.19
mgmtip=192.168.70.19
mgmtport=443
Aha! We were right – an ASCII-based, newline-delimited protocol.
啊哈!我们是对的 – 一个基于 ASCII 的、换行分隔的协议。
We can see some kind of header (the ‘6’, the non-ASCII character, and the ‘N’) and then some kind of command – ‘get auth’ – followed by a few variables.
我们可以看到某种标头(’6’、非 ASCII 字符和 ‘N’),然后是某种命令 – ‘get auth’ – 后跟一些变量。
Let’s look at that header in more detail.
让我们更详细地看一下该标头。
$ openssl s_server -port 123 --key key.pem --cert cert.pem --ign_eof -ignore_unexpected_eof -quiet | hexdump -C &
[1] 1522
$ socat TCP-CONNECT:127.0.0.1:123 TCP-CONNECT:192.168.70.19:541
00000000 36 e0 11 00 00 00 00 4e 67 65 74 20 61 75 74 68 |6......Nget auth|
00000010 0d 0a 66 67 5f 69 70 3d 31 39 32 2e 31 36 38 2e |..fg_ip=192.168.|
00000020 37 30 2e 31 39 0d 0a 6d 67 6d 74 69 70 3d 31 39 |70.19..mgmtip=19|
00000030 32 2e 31 36 38 2e 37 30 2e 31 39 0d 0a 6d 67 6d |2.168.70.19..mgm|
00000040 74 70 6f 72 74 3d 34 34 33 0d 0a 0d 0a |tport=443.......|
Huh, okay, this is interesting. We can draw a few conclusions from the data just by looking at it:
嗯,好吧,这很有趣。仅通过查看数据,我们就可以从数据中得出一些结论:
- The packet starts with four bytes we don’t know the purpose of yet –
36 e0 11 00
数据包以四个字节开头,我们还不知道其用途 –36 e0 11 00
- Following this are the bytes
00 00 00 4e
, which look to signify the length of the packet (although it seems to be slightly off!)
紧随其后的是字节00 00 00 4e
,它们看起来表示数据包的长度(尽管它似乎略有偏差! - Individual lines are terminated with “\r\n” (ie,
0d 0a
)
各行以 “\r\n” 结尾(即0d 0a
) - The entire packet is terminated with “\r\n\r\n” (ie,
0d 0a 0d 0a
)
整个数据包以“\r\n\r\n”结尾(即0d 0a 0d 0a
) - The first line looks to be some kind of command (
get auth
)
第一行看起来是某种命令 (get auth
) - Other lines appear to be key-value pairs, delimited by the equals sign,
=
其他行似乎是键值对,由等号=
分隔
It’s worth remembering that, at this point, we haven’t authenticated (or even sent any data) – this information is accessible to anyone who connects to the device.
值得记住的是,在这一点上,我们还没有进行身份验证(甚至没有发送任何数据)——连接到设备的任何人都可以访问此信息。
Looking back at (a cleaned up version of) the suspicious code, things start to make a little sense:
回顾一下可疑代码(清理的版本),事情开始变得有点意义了:
int sub_AD9D40(a1, a2)
{
v3 = get_property_value(a2, "authip");
if (v3 == NULL)
v3 = get_property_value(a2, "fmg_fqdn");
if (v3 == NULL)
v3 = get_property_value(a2, "mgmtip");
if ( v3 != NULL)
snprintf(a1->prop_204, 0x7F, v3->value);
It looks like the vulnerable code is searching for one of three properties – authip
, fmg_fqdn
, and mgmtip
– and if one is present, copying it into some data structure (via an insecure snprintf
call). Looking at the function that calls this, we learn a little more:
看起来易受攻击的代码正在搜索三个属性之一 – authip
、fmg_fqdn
和 mgmtip
– 如果存在,则将其复制到某个数据结构中(通过不安全的 snprintf
调用)。看看调用 this 的函数,我们学到更多:
requestInfo = get_property_value(a3, "request");
if ( requestInfo == NULL || (strcmp(requestInfo->value, "keepalive") != 0) )
{
if ( a2->prop_392 == 3 || a2->prop_392 == 0 )
return;
if ( strcmp(a3->value, "200") == 0 )
{
char* requestBody = (char*)&requestInfo[8];
if ( a2->prop_392 == 1 )
{
if ( strcmp(requestBody, "auth") != 0 )
return;
getUsernameAndPasswordFromPacket(a1, a2, a3);
if ( (a2->prop_133 & 1) == 0 )
sub_AD9D40(a2, a3);
With a little scrying, this starts to look straightforward – we get the request
key-value-pair from the packet, and if it is present (but is not keepalive
– remember that strcmp
returns 0 if the string matches), we check some value named prop_392
.
通过一点点的占卜,这开始看起来很简单 – 我们从数据包中获取请求
键值对,如果它存在(但不是keepalive
– 请记住,如果字符串匹配,strcmp
返回 0),我们检查一个名为 prop_392
的值。
Then, we check if the as-yet-unknown value is set to 200
, and if it does not, then we proceed to examine the data at position 8 of the packet. This makes sense, as the packet has an 8 byte header (if you’re unsure, refer to the hexdump above – the main get auth
command starts at position 0x08).
然后,我们检查 as-yet-unknown 值是否设置为 200
,如果没有,则我们继续检查数据包位置 8 的数据。这是有道理的,因为数据包有一个 8 字节的报头(如果您不确定,请参阅上面的 hexdump – 主 get auth
命令从位置 0x08 开始)。
We then check another property of the state object, prop_392
, and if it is set to 1, we proceed. The next thing that we do is ensure that the value of the request line is auth
, returning if not. Then, there’s a call to a function that extracts the user
and passwd
parameters, and finally – if the prop_133
property has it’s lowest bit set to zero – we invoke our suspicious function.
然后我们检查 state 对象的另一个属性 prop_392
,如果它设置为 1,我们继续。接下来我们要做的是确保 request 行的值为 auth
,否则返回。然后,调用一个函数来提取 user
和 passwd
参数,最后 – 如果 prop_133
属性的最低位设置为零 – 我们调用可疑函数。
This all sounds very complex but it boils down to the following packet:
这一切听起来非常复杂,但归结为以下数据包:
request=auth
authip=<some format string payload>
Since this packet is missing the first line (something analoguous to the get auth
that we saw originate from the FortiGate before), a little more legwork is in order.
由于这个数据包缺少第一行(类似于我们之前看到的来自 FortiGate 的 get auth
),因此需要做更多的跑腿工作。
If we look at the function that invokes this function, named fgfm_clt_handler
, we soon find the function responsible for parsing this command, and that the value required to hit our suspicious function is reply
.
如果我们查看调用此函数的函数(名为 fgfm_clt_handler
),我们很快就会发现负责解析此命令的函数,并且命中可疑函数所需的值是 reply
。
Some further examination reveals that the mysterious 200
we saw above corresponds to the value after the reply
token, meaning our packet should look like this:
进一步检查后发现,我们上面看到的神秘 200
对应于回复
令牌之后的值,这意味着我们的数据包应该看起来像这样:
reply 200
request=auth
authip=<some format string payload>
A little experimentation in the debugger, and we come up with the following exploit code (implemented in Python for ease of readability):
在调试器中进行一些实验,我们得出了以下漏洞利用代码(为了便于阅读,用 Python 实现):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
sock.connect((hostname, 541))
with context.wrap_socket(sock, server_side=True) as ssock:
# Read the packet from the target
pktFlags = struct.unpack('i', ssock.recv(4, 0))[0]
pktLen = struct.unpack('i', ssock.recv(4, 0))[0]
pktLen = pktLen - 2 # IDK why this is always wrong.
payload = ssock.recv(pktLen - 8)
# Now send some data. Send a request to set the authip to '%n'.
payload = b"reply 200\\r\\nrequest=auth\\r\\nauthip=%n\\r\\n\\r\\n\\x00"
packet = b''
packet += 0x0001e034.to_bytes(4, 'little')
packet += (len(payload) + 8 ).to_bytes(4, 'big')
packet += payload
ssock.send(packet)
You’ll note that we send the format-string payload of ‘%n’, which (as those familiar with format-string exploitation will know) will cause snprintf
to output the number of characters written so far into a variable.
你会注意到,我们发送了 ‘%n’ 的格式字符串有效负载,这(熟悉格式字符串利用的人会知道)将导致 snprintf
将到目前为止写入的字符数输出到变量中。
This is a little counter-intuitive, since format string modifiers usually control how data is displayed, so allow us to present a quick example:
这有点违反直觉,因为格式字符串修饰符通常控制数据的显示方式,所以请允许我们举一个简单的例子:
int foo;
printf("Hello%n", &foo)
After running this code, printf
will set the foo
variable to 5 (the number of characters it has emitted). This somewhat-strange behaviour is loved by format string exploiters everywhere, as it allows them to tamper with memory.
运行此代码后,printf
会将 foo
变量设置为 5(它发出的字符数)。这种有点奇怪的行为受到各地格式字符串利用者的喜爱,因为它允许他们篡改内存。
Since our FortiGate doesn’t actually specify any arguments for the snprintf
call, however, the data will be written to wherever address happens to be lying around on the stack. We’d expect this have a disastrous effect, which we can then adapt into a stable exploit.
然而,由于我们的 FortiGate 实际上并没有为 snprintf
调用指定任何参数,因此数据将被写入地址恰好位于堆栈上的任何位置。我们预计这会产生灾难性的影响,然后我们可以将其调整为稳定的漏洞利用。
We’re slightly surprised, then, by the behaviour of our exploit when run against a vulnerable instance – we see no crash. However, the connection is closed by the FortiGate – something that does not happen if we send a legitimate value in place of our %n
. Let’s dig into the snprintf
implementation to see if we can spot any clues.
然后,当我们的漏洞利用针对易受攻击的实例运行时的行为,我们感到有点惊讶 – 我们没有看到崩溃。但是,连接被 FortiGate 关闭 – 如果我们发送合法值来代替 %n
,则不会发生这种情况。让我们深入研究一下 snprintf
实现,看看我们是否能发现任何线索。
The snprintf
family of functions look somewhat terrifying in IDA – the one want is __vfprintf_internal
, which is a whopping 12KB large and looks like this:
snprintf
系列函数在 IDA 中看起来有点可怕 – 唯一需要的是 __vfprintf_internal
,它有 12KB 大,看起来像这样:
Yikes! 哎呀!
Fortunately, we don’t need to understand everything that goes on inside that monster – rather, we can just skim through and look for textual references. We spot this, which seems relevant to our interests:
幸运的是,我们不需要理解那个怪物内部发生的一切——相反,我们可以简单地浏览并寻找文本参考。我们发现了这一点,这似乎与我们的兴趣相关:
A little reading of the glibc source reveals that this is an exploitation mitigation, enabled by _FORTIFY_SOURCE
, intended to hinder clean exploitation of exactly this vulnerability class. If a %n
is detected to a writable segment, the handler will send a SIGABRT and abort the current task, instead of proceeding.
稍微阅读一下 glibc 源代码就会发现,这是由 _FORTIFY_SOURCE
启用的漏洞利用缓解措施,旨在阻止对这一漏洞类别的干净利用。如果检测到 %n
到可写段,处理程序将发送 SIGABRT 并中止当前任务,而不是继续。
This makes a lot of sense – since we’re seeing our connection close when we send a %n
payload, it makes a lot of sense that this is caused by glibc sending a signal to the process responsible for the connection.
这很有意义 – 因为当我们发送 %n
有效负载时,我们看到我们的连接关闭,所以这是由 glibc 向负责连接的进程发送信号引起的,这很有意义。
This may be bad news for exploit developers, requiring them to jump through more hoops to get a stable exploit, but it is actually great news for us – if you recall, the only reason we wanted to exploit the target in the first place was to firmly ascertain that is is vulnerable.
这对漏洞利用开发人员来说可能是个坏消息,需要他们跳过更多的障碍才能获得稳定的漏洞利用,但实际上这对我们来说是个好消息 – 如果你还记得,我们最初想要利用目标的唯一原因是为了牢牢确定它是易受攻击的。
Since sending a %n
doesn’t destabilise the target Fortigate device, but does immediately abort the connection, we can use this to very easily check if a device is patched – we can simply send a %n
, and if the connection aborts, the device is vulnerable.
由于发送 %n
不会破坏目标 Fortigate 设备的稳定性,但会立即中止连接,因此我们可以使用它来非常轻松地检查设备是否已修补 – 我们只需发送 %n
,如果连接中止,则设备易受攻击。
If the connection does not abort, then we know the device has been patched.
如果连接没有中止,则我们知道设备已修补。
Magical! 神奇!
We Observe, We Compare 我们观察,我们比较
We quickly wrote up a Python script to test for the vulnerability. Since we’re ‘thorough’ kind of people, we also decided to test against the two other branches of the Fortigate device, 7.4 and 7.0 – and boy, we’re glad we did!
我们迅速编写了一个 Python 脚本来测试漏洞。由于我们是“彻底”的人,我们还决定针对 Fortigate 设备的另外两个分支 7.4 和 7.0 进行测试 – 天哪,我们很高兴我们做到了!
Look what happens when we run our detection script against a vulnerable 7.4-series device (7.4.2, to be precise):
看看当我们针对易受攻击的 7.4 系列设备(准确地说是 7.4.2)运行检测脚本时会发生什么情况:
$ python CVE-2024-23113.py
....
self._sslobj.do_handshake()
ssl.SSLError: [SSL: TLSV1_ALERT_UNKNOWN_CA] tlsv1 alert unknown ca (_ssl.c:997)
Eh?! What’s happening here?!
啊?!这里发生了什么?!
Well, It looks like Fortinet added some kind of certificate validation logic in the 7.4 series, meaning that we can’t even connect to it (let alone send our payload) without being explicitly permitted by a device administrator. We also checked the 7.0 branch, and here we found things even more interesting, as an unpatched instance would allow us to connect with a self-signed certificate, while a patched machine requires a certificate signed by a configured CA.
嗯,看起来 Fortinet 在 7.4 系列中添加了某种证书验证逻辑,这意味着如果没有设备管理员的明确许可,我们甚至无法连接到它(更不用说发送我们的有效负载了)。我们还检查了 7.0 分支,在这里我们发现事情更有趣,因为未修补的实例将允许我们使用自签名证书进行连接,而修补的计算机需要由配置的 CA 签名的证书。
We did some reversing and determined that the certificate must be explicitly configured by the administrator of the device, which limits exploitation of these machines to the managing FortiManager instance (which already has superuser permissions on the device) or the other component of a high-availability pair.
我们做了一些反向操作,并确定证书必须由设备管理员显式配置,这将这些机器的利用限制在管理 FortiManager 实例(已经在设备上拥有超级用户权限)或高可用性对的其他组件。
It is not sufficient to present a certificate signed by a public CA, for example.
例如,仅提供由公有 CA 签名的证书是不够的。
Our experimentation yielded the following results:
我们的实验产生了以下结果:
Version 版本 | Status 地位 | Behaviour 行为 |
---|---|---|
7.0.13 | Vulnerable 脆弱 | Accepts self-signed cert 接受自签名证书 |
7.0.14 | Patched 修补 | Requires cert signed by configured CA 需要由配置的 CA 签名的证书 |
7.2.6 | Vulnerable 脆弱 | Accepts self-signed cert 接受自签名证书 |
7.2.7 | Patched 修补 | Accepts self-signed cert 接受自签名证书 |
7.4.2 | Vulnerable 脆弱 | Requires cert signed by configured CA 需要由配置的 CA 签名的证书 |
7.4.3 | Patched 修补 | Requires cert signed by configured CA 需要由配置的 CA 签名的证书 |
This leaves us in something of a predicament.
这让我们陷入了某种困境。
While we can accurately ascertain the status of a given 7.2-branch device, or even a 7.0-branch device, we can’t tell if a given 7.4-branch device is patched or not. Worse, we cannot distinguish between a patched 7.0-series device and a 7.4 device!
虽然我们可以准确地确定给定的 7.2 分支设备甚至 7.0 分支设备的状态,但我们无法判断给定的 7.4 分支设备是否已修补。更糟糕的是,我们无法区分修补的 7.0 系列设备和 7.4 设备!
How can we remedy this?
我们该如何补救呢?
Well, it turns out that there are additional changes introduced in the patch that we can leverage.
好吧,事实证明,我们可以利用补丁中引入的其他更改。
If we start openssl
with the -trace
argument, we’ll get a nice printout of the gory details of the TLS negotiation.
如果我们使用 -trace
参数启动 openssl
,我们将得到 TLS 协商的血腥细节的漂亮打印输出。
Doing so against the patched and vulnerable instances, we spot a key difference between the two – the ‘certificate_authorities’ extension is only returned on patched instances!
对已修补和易受攻击的实例执行此操作,我们发现了两者之间的关键区别 – “certificate_authorities”扩展仅在修补的实例上返回!
$ socat TCP-CONNECT:127.0.0.1:123 TCP-CONNECT:<host>:541
Header:
Version = TLS 1.0 (0x301)
Content Type = Handshake (22)
Length = 312
ClientHello, Length=308
...
extensions, length = 201
...
extension_type=certificate_authorities(47), length=652
0000 - 02 8a 00 89 30 81 86 31-0b 30 09 06 03 55 04 ....0..1.0...U.
...
0276 - 16 14 73 75 70 70 6f 72-74 40 66 6f 72 74 69 ..support@forti
0285 - 6e 65 74 2e 63 6f 6d net.com
...
Thinking we can use this information to determine if a host is patched, we did some additional testing, and found that the extension is not sent for all branches – let’s expand our results table to show what sends the ‘certificate_authorities’ extension:
考虑到我们可以使用此信息来确定主机是否已修补,我们进行了一些额外的测试,发现并非所有分支都发送了扩展 – 让我们展开结果表以显示发送 ‘certificate_authorities’ 扩展的内容:
Version 版本 | Status 地位 | ‘certificate_authorities’ extension status “certificate_authorities”扩展状态 |
Behaviour 行为 |
---|---|---|---|
7.0.13 | Vulnerable 脆弱 | Absent 缺席 | Accepts self-signed cert 接受自签名证书 |
7.0.14 | Patched 修补 | Present 目前 | Requires cert signed by configured CA 需要由配置的 CA 签名的证书 |
7.2.6 | Vulnerable 脆弱 | Absent 缺席 | Accepts self-signed cert 接受自签名证书 |
7.2.7 | Patched 修补 | Absent 缺席 | Accepts self-signed cert 接受自签名证书 |
7.4.2 | Vulnerable 脆弱 | Absent 缺席 | Requires cert signed by configured CA 需要由配置的 CA 签名的证书 |
7.4.3 | Patched 修补 | Present 目前 | Requires cert signed by configured CA 需要由配置的 CA 签名的证书 |
As you can see, the presence (or absence) of the ‘certificate_authorities’ extension is not a completely reliable indicator of whether the device has been patched.
如您所见,“certificate_authorities”扩展的存在(或不存在)并不是设备是否已修补的完全可靠指标。
However, it can be combined with the result of our previous test to determine if a device is patched to reliably detect if a device is patched – even for those difficult-to-test branches which require a valid TLS cert before connecting.
但是,它可以与我们之前测试的结果相结合,以确定设备是否已修补,从而可靠地检测设备是否已修补 – 即使是那些在连接前需要有效 TLS 证书的难以测试的分支。
Therefore, we can detect these cases even without successfully connecting to the target. Here’s a flowchart to show the detection logic:
因此,即使没有成功连接到目标,我们也可以检测到这些情况。下面是显示检测逻辑的流程图:
Using this logic, we can sort devices into three categories:
使用这个逻辑,我们可以将设备分为三类:
- Patched. 修补。
- Vulnerable, but requires a trusted certificate.
易受攻击,但需要受信任的证书。 - Vulnerable, and will accept a self-signed certificate.
易受攻击,并将接受自签名证书。
It is clear that ‘patched’ and ‘unpatched’ are vague terms in this context, and more nuance is required.
很明显,在这种情况下,“patched”和“unpatched”是模糊的术语,需要更多的细微差别。
How Critical Is ‘Critical’?
“关键”有多重要?
A key role in managing an organisation’s attack surface is the ability to confidently advise the client about the urgency of patching devices.
管理组织攻击面的一个关键作用是能够自信地向客户建议修补设备的紧迫性。
Remember, we’re talking about a routing appliance here, not a desktop (or even a server) – patching is not a simple operation, often requiring a maintenance window to be declared, fallback plans created, and other extra work. Sysadmins are often asking that all-important question – ‘just how critical is this critical update’?
请记住,我们在这里谈论的是路由设备,而不是桌面(甚至服务器)——修补不是一项简单的操作,通常需要声明维护时段、创建回退计划以及其他额外工作。系统管理员经常会问一个非常重要的问题 – “这个关键更新到底有多重要”?
Fortinet’s advice here is simply to update, which is always sound advice, but doesn’t really communicate the nuance of this vulnerability.
Fortinet 在这里的建议只是更新,这始终是合理的建议,但并没有真正传达此漏洞的细微差别。
I mean, it’s one up from Checkpoint’s ‘buy another Checkpoint device to put infront of your vulnerable Checkpoint device’
我的意思是,它比 Checkpoint 的“购买另一台 Checkpoint 设备放在易受攻击的 Checkpoint 设备前面”有所提升
Assuming an organisation is unable to apply the supplied workaround, the urgency of upgrade is largely dictated by the willingness of the target to accept a self-signed certificate.
假设组织无法应用提供的解决方法,则升级的紧迫性在很大程度上取决于目标是否愿意接受自签名证书。
Targets that will do so are open to attack by any host that can access them, while those devices that require a certificate signed by a trusted root are rendered unexploitable in all but the narrowest of cases (because the TLS/SSL ecosystem is just so solid, as we recently demonstrated).
这样做的目标很容易受到任何可以访问它们的主机的攻击,而那些需要由受信任根签名的证书的设备在除最少数情况下外都无法被利用(因为 TLS/SSL 生态系统非常可靠,正如我们最近所证明的那样)。
Conclusion 结论
Well, that was an interesting rabbit-hole of appliance behaviour stemming from an otherwise-unremarkable vulnerability!
嗯,这是一个有趣的设备行为兔子洞,源于一个原本不起眼的漏洞!
It goes to show just how often there is hidden complexity lurking behind the façade of simplicity – this case, “you should update to fix this critical vulnerability”.
它表明,在简单的表象背后隐藏着复杂性是多么频繁——在这种情况下,“你应该更新以修复这个关键漏洞”。
While it’s always a good idea to update to the latest version, the life of a sysadmin is filled with cost-to-benefit analysis, juggling the needs of users with their best interests. While we’d like to have seen a more detailed explanation of risk from Fortinet, covering the three currently-maintained branches, we can understand their desire for their customers to patch all branches to patch to current regardless.
虽然更新到最新版本总是一个好主意,但系统管理员的一生中充满了成本收益分析,在用户的需求和他们的最大利益之间取得平衡。虽然我们希望看到 Fortinet 对风险的更详细解释,涵盖当前维护的三个分支,但我们可以理解他们希望客户无论如何都要修补所有分支以修补到当前分支。
Still, it is somewhat troubling when third parties need to reverse patches to uncover such details.
尽管如此,当第三方需要反转补丁以发现这些细节时,这还是有些令人不安的。
Here at watchTowr, we believe continuous security testing is the future, enabling the rapid identification of holistic high-impact vulnerabilities that affect your organisation.
在 watchTowr,我们相信持续安全测试是未来趋势,能够快速识别影响您组织的整体高影响漏洞。
原文始发于Aliz Hammond:Fortinet FortiGate CVE-2024-23113 – A Super Complex Vulnerability In A Super Secure Appliance In 2024
转载请注明:Fortinet FortiGate CVE-2024-23113 – A Super Complex Vulnerability In A Super Secure Appliance In 2024 | CTF导航