
Analyse technique de CVE-2026-88776, un underflow d'entier pré-authentification dans le parseur d'enregistrements Oracle TNS de nsppe de NetScaler, provoquant une écriture hors limites et un plantage du moteur de paquets.
NetScaler peut répartir la charge des bases de données Oracle. Pointez un vserver de type ORACLE vers lui et le moteur de paquets se met à parler lui-même le protocole Oracle TNS, analysant le trafic client avec un gestionnaire intégré. Ce gestionnaire maintient une petite machine à états par connexion afin de se souvenir de sa position dans un enregistrement TNS.
L'un de ces états signifie « je suis au milieu d'un enregistrement, d'autres octets arrivent ». Lorsque le segment suivant arrive, le gestionnaire lit sa longueur et soustrait 10, car 10 octets de chaque segment constituent l'en-tête.
Personne n'a jamais demandé ce qui se passe si le segment fait moins de 10 octets.
Envoyez-en un. Un segment avorton de 1 à 9 octets entre dans cet état, la longueur devient négative, et comme la longueur réside dans un registre non signé de 16 bits, elle ne devient pas négative. Elle déborde vers une valeur énorme, jusqu'à environ 64 Ko. Le gestionnaire effectue alors allègrement un memcpy de cette quantité de données depuis votre paquet vers un tampon de travail fixe de 0x748 octets dans .bss. Le moteur n'est pas PIE, donc l'adresse piétinée ne bouge jamais.
Vous n'avez pas besoin de vous authentifier. Vous n'avez même pas besoin d'une base de données derrière le vserver, car l'appliance répond elle-même à la poignée de main TNS. L'attaque entière tient en une seule conversation TCP.
Les deux volets ont été démontrés en direct :
Le gestionnaire stocke son état par connexion à [pcb+0x15a]. La plupart des états sont sans intérêt. Celui qui compte ici est 0x1F4, qui signifie « milieu d'enregistrement, en attente de continuation », et vous dirigez une connexion vers cet état en n'utilisant que des octets de protocole ordinaires :
record type 6 -> deep-parser state 6 -> byte 0x03 (TTIFUN)
-> state 0x126 -> TTC function byte 0x5e (OALL8)
-> jump table 0x23589e0, index 0x59 -> writer at bfe27b:
[pcb+0x15a] = 0x1F4
Le paquet suivant sur cette connexion est ensuite distribué sur 0x1F4, via bd49a7 jusqu'à bd5362, et aboutit ici :
bd5432: memset(0x30a7e60, 0, 0x748) ; scratch region (ends 0x30a85a8)
bd5445: mov rcx,[rbp-0x208] ; inbound packet buffer
bd544c: movzx eax,WORD [rcx+0xf2] ; u16 wire length of the segment
bd5453: add eax,0xfffffff6 ; len = len - 10 <== THE UNDERFLOW
bd5456: mov WORD [0x30a8476],ax ; wrapped length PERSISTED to .bss
bd545d: movzx edx,ax ; memcpy length (1..9 -> 0xfff7..0xffff)
bd5460: mov rsi,[rcx+0xe8]
bd5467: add rsi,0xa ; src = payload + 10
bd546b: mov edi,0x30a8075 ; dst = FIXED .bss global
bd5470: call 1b075e0 ; memcpy: up to ~64 KB OOB write
bd5475: mov BYTE [0x30a8478],1
Lisez-le comme une phrase. Prenez la longueur du segment. Soustrayez 10. Stockez le résultat, passez-le à memcpy, terminé. À aucun moment on ne se demande si « longueur moins 10 » est encore un nombre qui a du sens.
Nous avons compté chaque référence à ce champ de longueur dans cette fonction. Trente. Exactement une est une vérification, à bd4083, et tout ce qu'elle fait est de comparer à zéro. Un segment de longueur nulle est rejeté. Toute autre longueur, y compris de 1 à 9, file droit dans la soustraction.
Le code croit manifestement que la machine à états aura livré un enregistrement suffisamment complet au moment où l'état 0x1F4 s'exécute. La machine à états n'a jamais promis une telle chose. Un segment tronqué arrivant comme son propre paquet satisfait toutes les conditions de dispatch et se présente cinq octets trop court.
Le plus exaspérant, c'est que le fournisseur connaît cette classe de bug. Ailleurs dans ce même binaire, sub_119c940 effectue l'arithmétique identique de longueur moins constante et la protège avec cmp eax,0x3f9 / ja. Le motif sûr était juste là. Ce site ne l'a tout simplement pas utilisé.
La destination, 0x30a8075, se trouve à l'intérieur d'une région de travail de 0x748 octets avec seulement 0x533 octets d'espace légitime devant elle. La copie débordante dépasse celle-ci et se répand dans les globales DataStream et de négociation voisines, avec vos octets. Et la longueur débordante elle-même est persistée à 0x30a8476, où du code ultérieur la relit et lui fait confiance. C'est une seconde voie de corruption, à un pas du memcpy.
La configuration tient en une ligne, un déploiement DataStream standard :
add lb vserver v ORACLE <ip> 1521
Aucun backend n'est nécessaire. L'appliance termine elle-même le TNS et construit son propre paquet Accept, avec la chaîne de fournisseur « Netscaler » et tout le reste.
La conversation se déroule en quatre étapes :
1. TNS Connect (version 0x13A)
<- the appliance answers Resend(11), then Accept(2), 32 bytes, v0x139
2. one complete, benign TTC round-trip
(some builds like to glue your runt onto the previous segment;
this resets the segmentation so the runt lands alone)
3. the arming segment: a TNS Data packet whose declared length is
actual + 32, carrying TTC bytes {03 5E} (TTIFUN / OALL8)
-> the state machine latches [pcb+0x15a] = 0x1F4, mid-record
4. the runt: 1-9 bytes, sent as its own TCP segment
-> passes the zero check, dispatches on 0x1F4
-> length - 10 wraps
-> memcpy(0x30a8075, payload+10, ~0xFFF6)
-> the packet engine dies
Les étapes 1 à 3 sont de la pure conversation. L'étape 4 fait cinq octets.
Cela a été déclenché deux fois sur un appliance 14.1-73.30, même machine, même build, deux exécutions indépendantes. Les deux fois, le moteur de paquets a reçu un SIGSEGV avec un core dump correspondant, ns.log a enregistré le signal 11 dans la fenêtre, et le plan de données est resté indisponible pendant le redémarrage à chaud d'environ 60 secondes.
Le contrôle négatif a réussi lors des deux exécutions. Exécutez la conversation identique mais faites du segment final 10 octets cohérents et rien ne se produit. Le moteur le traite et continue. La longueur tronquée est la seule variable qui compte.