
Technische Analyse von CVE-2026-88776, einem Pre-Auth-Integer-Underflow im Oracle-TNS-Record-Parser von NetScaler nsppe, der einen Out-of-Bounds-Write und einen Absturz der Packet Engine verursacht.
NetScaler kann Oracle-Datenbanken per Load-Balancing verteilen. Richtet man einen vserver vom Typ ORACLE darauf, beginnt die Packet Engine selbst das Oracle-TNS-Protokoll zu sprechen und parst den Client-Verkehr mit einem eingebauten Handler. Dieser Handler unterhält pro Verbindung eine kleine Zustandsmaschine, damit er sich merkt, wo innerhalb eines TNS-Records er sich befindet.
Einer dieser Zustände bedeutet: „Ich bin mitten in einem Record, es kommen weitere Bytes." Wenn das nächste Segment eintrifft, liest der Handler dessen Länge und subtrahiert 10, denn 10 Bytes jedes Segments sind Header.
Niemand hat je gefragt, was passiert, wenn das Segment kürzer als 10 Bytes ist.
Schickt ihm eines. Ein Zwergsegment von 1 bis 9 Bytes läuft in diesen Zustand, die Länge wird negativ, und weil die Länge in einem vorzeichenlosen 16-Bit-Register liegt, wird sie nicht negativ. Sie läuft über zu einem gewaltigen Wert, bis zu etwa 64 KB. Der Handler memcpy't dann fröhlich diese Menge an Daten aus dem Paket in einen festen Scratch-Puffer von 0x748 Bytes in .bss. Die Engine ist nicht PIE, sodass sich die überschriebene Adresse nie verschiebt.
Man muss sich nicht anmelden. Man braucht nicht einmal eine Datenbank hinter dem vserver, denn die Appliance beantwortet den TNS-Handshake selbst. Der gesamte Angriff ist eine einzige TCP-Konversation.
Beide Hälften wurden live demonstriert:
Der Handler speichert seinen Zustand pro Verbindung unter [pcb+0x15a]. Die meisten Zustände sind langweilig. Der hier relevante ist 0x1F4, was „mitten im Record, warte auf Fortsetzung" bedeutet, und man steuert eine Verbindung dorthin allein mit gewöhnlichen Protokoll-Bytes:
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
Das nächste Paket auf dieser Verbindung verzweigt dann bei 0x1F4, über bd49a7 nach bd5362, und landet hier:
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
Lies es wie einen Satz. Nimm die Länge des Segments. Subtrahiere 10. Speichere das Ergebnis, übergib es an memcpy, fertig. An keiner Stelle wird gefragt, ob „Länge minus 10" noch eine Zahl ist, die Sinn ergibt.
Wir haben jeden Verweis auf dieses Längenfeld in dieser Funktion gezählt. Dreißig davon. Genau einer ist eine Prüfung, bei bd4083, und alles, was sie tut, ist ein Vergleich mit null. Ein Segment der Länge null wird abgelehnt. Jede andere Länge, einschließlich 1 bis 9, läuft direkt in die Subtraktion.
Der Code geht offensichtlich davon aus, dass die Zustandsmaschine bis zum Zeitpunkt, an dem Zustand 0x1F4 ausgeführt wird, einen ausreichend vollständigen Datensatz geliefert haben wird. Die Zustandsmaschine hat nie etwas Derartiges versprochen. Ein zu kurzes Segment, das als eigenes Paket ankommt, erfüllt jede Dispatch-Bedingung und taucht fünf Bytes zu kurz auf.
Der wirklich ärgerliche Teil ist, dass der Hersteller diese Fehlerklasse kennt. An anderer Stelle in genau demselben Binary führt sub_119c940 die identische Arithmetik Länge-minus-Konstante aus und schützt sie mit cmp eax,0x3f9 / ja. Das sichere Muster lag direkt vor der Nase. Diese Stelle hat es einfach nicht verwendet.
Das Ziel, 0x30a8075, liegt innerhalb eines 0x748 Byte großen Scratch-Bereichs, mit nur 0x533 Byte legitimem Platz davor. Die umgebrochene Kopie schießt darüber hinaus in die DataStream- und Negotiation-Globals nebenan, mit deinen Bytes. Und die umgebrochene Länge selbst wird nach 0x30a8476 persistiert, wo späterer Code sie zurückliest und ihr vertraut. Das ist eine zweite Korruptionsspur, einen Schritt entfernt vom memcpy.
Das Setup ist eine Zeile Konfiguration, ein Standard-DataStream-Deployment:
add lb vserver v ORACLE <ip> 1521
Kein Backend erforderlich. Die Appliance beendet TNS selbst und baut ihr eigenes Accept-Paket, inklusive der Vendor-Zeichenkette „Netscaler".
Die Konversation durchläuft vier Schritte:
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
Die Schritte 1 bis 3 sind reine Konversation. Schritt 4 besteht aus fünf Bytes.
Dies wurde zweimal auf einer 14.1-73.30-Appliance ausgelöst, dieselbe Box, derselbe Build, zwei unabhängige Durchläufe. Beide Male erhielt die Paket-Engine SIGSEGV mit einem passenden Core-Dump, ns.log verzeichnete Signal 11 innerhalb des Zeitfensters, und die Dataplane blieb für den etwa 60-sekündigen Warm-Neustart außer Betrieb.
Die Negativkontrolle bestand beide Durchläufe. Führt man die identische Konversation aus, aber macht das letzte Segment zu kohärenten 10 Bytes, passiert nichts. Die Engine verarbeitet es und macht weiter. Die Länge des Runt ist die einzige Variable, die zählt.