
Analisi tecnica di CVE-2026-88776, un integer underflow pre-autenticazione nel parser dei record Oracle TNS di nsppe di NetScaler che causa una scrittura fuori dai limiti e il crash del packet engine.
NetScaler è in grado di bilanciare il carico dei database Oracle. Basta puntare un vserver di tipo ORACLE verso di esso e il packet engine inizia a parlare direttamente il protocollo Oracle TNS, analizzando il traffico dei client con un handler integrato. Tale handler mantiene una piccola macchina a stati per ogni connessione, così da ricordare in quale punto si trova all'interno di un record TNS.
Uno di questi stati significa "sono nel mezzo di un record, stanno arrivando altri byte". Quando arriva il segmento successivo, l'handler ne legge la lunghezza e sottrae 10, perché 10 byte di ogni segmento sono header.
Nessuno si è mai chiesto cosa succede se il segmento è più corto di 10 byte.
Inviane uno. Un segmento troncato da 1 a 9 byte entra in quello stato, la lunghezza diventa negativa e, poiché la lunghezza risiede in un registro a 16 bit senza segno, non diventa negativa. Va in wrap su un valore enorme, fino a circa 64 KB. L'handler esegue quindi tranquillamente una memcpy di quella quantità di dati dal tuo pacchetto in un buffer scratch fisso da 0x748 byte in .bss. Il motore non è PIE, quindi l'indirizzo che viene calpestato non si sposta mai.
Non devi effettuare il login. Non ti serve nemmeno un database dietro il vserver, perché l'appliance risponde da sola all'handshake TNS. L'intero attacco è una singola conversazione TCP.
Entrambe le metà sono state dimostrate dal vivo:
L'handler memorizza il proprio stato per connessione in [pcb+0x15a]. La maggior parte degli stati è banale. Quello che conta qui è 0x1F4, che significa "a metà record, in attesa di continuazione", e si guida una connessione verso di esso usando nient'altro che byte di protocollo ordinari:
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
Il pacchetto successivo su quella connessione viene quindi inviato su 0x1F4, attraverso bd49a7 fino a bd5362, e arriva qui:
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
Leggilo come una frase. Prendi la lunghezza del segmento. Sottrai 10. Memorizza il risultato, passalo a memcpy, fatto. In nessun momento qualcosa si chiede se "lunghezza meno 10" sia ancora un numero che abbia senso.
Abbiamo contato ogni riferimento a quel campo di lunghezza in questa funzione. Trenta. Esattamente uno è un controllo, a bd4083, e tutto ciò che fa è confrontare con zero. Un segmento di lunghezza zero viene rifiutato. Ogni altra lunghezza, inclusa da 1 a 9, finisce dritta nella sottrazione.
Il codice crede chiaramente che la macchina a stati abbia consegnato un record sufficientemente completo nel momento in cui viene eseguito lo stato 0x1F4. La macchina a stati non ha mai promesso nulla del genere. Un segmento troncato che arriva come pacchetto a sé stante soddisfa ogni condizione di dispatch e si presenta cinque byte più corto.
La parte davvero esasperante è che il fornitore conosce questa classe di bug. Altrove, nello stesso identico binario, sub_119c940 esegue la stessa aritmetica lunghezza-meno-costante e la protegge con cmp eax,0x3f9 / ja. Il pattern sicuro era lì a disposizione. Questo sito semplicemente non l'ha usato.
La destinazione, 0x30a8075, si trova all'interno di una regione scratch di 0x748 byte con solo 0x533 byte di spazio legittimo davanti ad essa. La copia andata in wrap supera quel limite e finisce nelle variabili globali DataStream e di negoziazione adiacenti, con i tuoi byte. E la lunghezza andata in wrap stessa viene persistita a 0x30a8476, dove il codice successivo la rilegge e se ne fida. Questa è una seconda corsia di corruzione, a un passo di distanza dalla memcpy.
La configurazione è una riga di configurazione, un deployment DataStream standard:
add lb vserver v ORACLE <ip> 1521
Nessun backend necessario. L'appliance termina TNS da sola e costruisce il proprio pacchetto Accept, stringa vendor "Netscaler" inclusa.
La conversazione si articola in quattro fasi:
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
I passaggi da 1 a 3 sono pura conversazione. Il passaggio 4 è di cinque byte.
Questo è stato attivato due volte su un'appliance 14.1-73.30, stesso dispositivo, stessa build, due esecuzioni indipendenti. In entrambi i casi il motore dei pacchetti ha ricevuto SIGSEGV con un core dump corrispondente, ns.log ha registrato il segnale 11 all'interno della finestra, e il dataplane è rimasto inattivo per il warm restart di circa 60 secondi.
Il controllo negativo è passato in entrambe le esecuzioni. Eseguendo la conversazione identica ma rendendo il segmento finale 10 byte coerenti, non accade nulla. Il motore lo elabora e prosegue. La lunghezza ridotta è l'unica variabile che conta.