
Análisis técnico de CVE-2026-88776, un desbordamiento negativo de enteros previo a la autenticación en el analizador de registros Oracle TNS de nsppe de NetScaler que provoca una escritura fuera de límites y un fallo del motor de paquetes.
NetScaler puede balancear la carga de bases de datos Oracle. Apunte un vserver de tipo ORACLE hacia él y el motor de paquetes comenzará a hablar el propio protocolo Oracle TNS, analizando el tráfico de los clientes con un manejador integrado. Ese manejador mantiene una pequeña máquina de estados por conexión para recordar en qué punto se encuentra dentro de un registro TNS.
Uno de esos estados significa "estoy en medio de un registro, vienen más bytes". Cuando llega el siguiente segmento, el manejador lee su longitud y le resta 10, porque 10 bytes de cada segmento son cabecera.
Nadie se preguntó nunca qué ocurre si el segmento tiene menos de 10 bytes.
Envíe uno. Un segmento diminuto de 1 a 9 bytes entra en ese estado, la longitud se vuelve negativa y, como la longitud reside en un registro sin signo de 16 bits, no se vuelve negativa. Se desborda a algo enorme, hasta aproximadamente 64 KB. El manejador entonces hace memcpy alegremente de esa cantidad de datos desde su paquete hacia un búfer temporal fijo de 0x748 bytes en .bss. El motor no es PIE, así que la dirección que se está pisoteando nunca se mueve.
No hace falta iniciar sesión. Ni siquiera necesita una base de datos detrás del vserver, porque el propio appliance responde al handshake TNS. Todo el ataque es una única conversación TCP.
Ambas mitades se demostraron en vivo:
El manejador almacena su estado por conexión en [pcb+0x15a]. La mayoría de los estados son aburridos. El que importa aquí es 0x1F4, que significa "en medio de un registro, esperando continuación", y se dirige una conexión hacia él usando nada más que bytes de protocolo ordinarios:
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
El siguiente paquete en esa conexión luego se despacha en 0x1F4, a través de bd49a7 hacia bd5362, y aterriza aquí:
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
Léelo como una frase. Toma la longitud del segmento. Réstale 10. Almacena el resultado, pásalo a memcpy, listo. En ningún momento nada pregunta si "longitud menos 10" sigue siendo un número que tenga sentido.
Contamos cada referencia a ese campo de longitud en esta función. Treinta de ellas. Exactamente una es una comprobación, en bd4083, y lo único que hace es comparar con cero. Un segmento de longitud cero es rechazado. Cualquier otra longitud, incluida de 1 a 9, entra directamente en la resta.
El código cree claramente que la máquina de estados habrá entregado un registro suficientemente completo para cuando se ejecute el estado 0x1F4. La máquina de estados nunca prometió tal cosa. Un segmento truncado que llega como su propio paquete satisface todas las condiciones de despacho y aparece cinco bytes más corto.
La parte verdaderamente exasperante es que el proveedor conoce esta clase de error. En otra parte del mismísimo binario, sub_119c940 realiza la misma aritmética de longitud menos constante y la protege con cmp eax,0x3f9 / ja. El patrón seguro estaba justo ahí. Este sitio simplemente no lo usó.
El destino, 0x30a8075, vive dentro de una región de scratch de 0x748 bytes con solo 0x533 bytes de espacio legítimo delante de él. La copia desbordada irrumpe más allá de eso en los globales de DataStream y de negociación de al lado, con tus bytes. Y la longitud desbordada en sí se persiste en 0x30a8476, donde el código posterior la lee de vuelta y confía en ella. Esa es una segunda vía de corrupción, a un paso del memcpy.
La configuración es una línea de configuración, un despliegue estándar de DataStream:
add lb vserver v ORACLE <ip> 1521
No se necesita backend. El appliance termina TNS por sí mismo y construye su propio paquete Accept, con la cadena de proveedor "Netscaler" y todo.
La conversación consta de cuatro pasos:
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
Los pasos 1 a 3 son pura conversación. El paso 4 son cinco bytes.
Esto se ha disparado dos veces en un appliance 14.1-73.30, misma máquina, misma compilación, dos ejecuciones independientes. En ambas ocasiones el motor de paquetes recibió SIGSEGV con un volcado de memoria coincidente, ns.log registró la señal 11 dentro de la ventana, y el plano de datos quedó inactivo durante el reinicio en caliente de ~60 segundos.
El control negativo pasó en ambas ejecuciones. Ejecute la conversación idéntica pero haga que el segmento final sea de 10 bytes coherentes y no ocurre nada. El motor lo procesa y continúa. La longitud reducida es la única variable que importa.