
Prueba de concepto y laboratorio que demuestra un DoS de envenenamiento de claves en el handshake de negociación de versión compatible con QUIC en MsQuic v2.6.1, con un testigo de inyección UDP y análisis de la corrección.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · msquic-compat-vn-key-poison
MsQuic v2.6.1 (a01333cf) - Microsoft
Se supone que la negociación de versión compatible debe esperar hasta que el cliente pueda procesar realmente un paquete de la nueva versión. Este árbol confirma la versión y recrea las claves Initial en el momento en que un long header parece compatible, y luego continúa. El "deshacer" por fallo de descifrado restaura el número de versión y deja las claves en v2. Un paquete de tipo Handshake o truncado ni siquiera llega a ese deshacer.
Un único datagrama UDP desde la 4-tupla del servidor detiene el handshake de un cliente por defecto. El proceso sigue en pie. Sin RCE.
| ID | sin CVE todavía |
| CWE | CWE-345, CWE-347, CWE-670 |
| CVSS | Alto: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| Producto | MsQuic |
| Afectado | v2.6.1 (a01333cf); seguía en main cuando lo comprobé |
| Auth | UDP no autenticado, el origen debe ser la 4-tupla del servidor (on-path, o IP spoof del servidor más el puerto del cliente) |
| Licencia | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Durante un handshake QUIC v1, envía un datagrama de long header originado desde la dirección y el puerto del servidor. La versión es QUIC v2 (RFC 9369, wire 6b 33 43 cf). DestCidLength es 0. Los clientes por defecto usan un socket UDP conectado exclusivo, por lo que aceptan un DCID de longitud cero. La biblioteca lo trata como negociación de versión compatible antes de AEAD, deriva nuevas claves Initial, y luego o bien nunca llega al revert por fallo de descifrado (el tipo Handshake se difiere; el Initial truncado devuelve FALSE primero) o bien revierte Stats.QuicVersion y deja las claves en v2.
El servidor real sigue hablando v1. El cliente retransmite con sales v2. Ese intento nunca llega a Connected. Otras conexiones en el mismo proceso no se ven afectadas. Esto es un DoS de handshake, no RCE.
Off-path requiere IP spoof del servidor más el puerto UDP efímero del cliente. On-path es suficiente. El binding exclusivo significa que el atacante no necesita el CID.
Primero leí la oleada de GHSA de 2026 sobre microsoft/msquic: MITM de hostname (GHSA-w5f4-fx9m-m4q7, este tag es el cherry-pick), RCE por path-UAF (GHSA-92f5-vc22-8j33), underflow de ACK (GHSA-gvvw-8j96-8g5r). Esos sinks están cerrados en v2.6.1.
Luego cabeceras de paquete, CID, negociación de versión, retry, stateless reset, CIBIR. La VN compatible de RFC 9368 es lo que queda. QuicConnRecvHeader ve un desajuste de versión, pregunta si es compatible, escribe OriginalQuicVersion, establece CompatibleVerNegotiationAttempted, asigna Stats.QuicVersion, llama a QuicCryptoOnVersionChange y continúa. El comentario dice que los transport parameters comprobarán ChosenVersion más tarde. Esos TPs nunca llegan si el handshake nunca descifra.
El fallo de descifrado parece una solución hasta que lo lees:
Connection->Stats.QuicVersion = Connection->OriginalQuicVersion;
Connection->State.CompatibleVerNegotiationAttempted = FALSE;
Sin un segundo QuicCryptoOnVersionChange. Las etiquetas HKDF se quedan en v2. Los paquetes de tipo Handshake se difieren (HANDSHAKE > ReadKey=INITIAL) y RecvHeader devuelve FALSE, así que ese deshacer nunca se ejecuta.
No necesité src/tools/attack. Siete bytes son suficientes.
Caminos equivocados ya registrados: inyectar en el puerto de escucha del servidor (el cliente está conectado de forma exclusiva, no lo verá); reenviar el Initial antes del poison en loopback (el handshake termina primero, el poison llega tarde); getchar() como espera del servidor de ejemplo (Docker no es un teclado); clon superficial con submódulo openssl vacío (la imagen tiene que hacer fetch de v2.6.1 e init de openssl); cualquier long header basura como prueba (el no compatible 11223344 sigue Connected - la parte interesante es la v2 compatible). Teatro: un payload CRYPTO, una reverse shell, RCE. El testigo es el resultado del handshake.
cd lab
./run.sh
El proxy UDP 127.0.0.1:18150 es la dirección a la que se conecta el cliente. El servidor quicsample real es 127.0.0.1:18151. En el primer datagrama del cliente, el PoC envía un Handshake v2 truncado f06b3343cf0000 desde el socket del proxy, y luego reenvía. Bíndalo a loopback.
control_connected=1
inject_connected=0
negative_connected=1
inject_hex=f06b3343cf0000
SUCCESS MSQUIC-COMPAT-VN-KEY-POISON-WITNESS
CONTROL es solo reenvío y debe dar Connected. NEGATIVE inyecta f0112233440000 primero y debe seguir dando Connected. INJECT es la única fase que muere.
No llames a QuicCryptoOnVersionChange hasta que el paquete que lo desencadena se descifre. En cada fallo tras un commit (RecvHeader FALSE y fallo de descifrado) restaura la versión y deriva las claves de vuelta a OriginalQuicVersion. Ignora Handshake / 0-RTT como desencadenantes de VN compatible. Solo un Initial descifrable de la nueva versión debería hacer el cambio.
connection.c · crypto.c · binding.c · version_neg.c