
Proof-of-Concept und Lab, das einen QUIC-kompatiblen Version-Negotiation-Key-Poison-Handshake-DoS in MsQuic v2.6.1 demonstriert, mit einem UDP-Injection-Nachweis und Fix-Analyse.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · msquic-compat-vn-key-poison
MsQuic v2.6.1 (a01333cf) - Microsoft
Die kompatible Versionsaushandlung soll eigentlich warten, bis der Client tatsächlich ein Paket der neuen Version verarbeiten kann. Dieser Tree übernimmt die Version und erzeugt die Initial-Keys neu, sobald ein Long Header kompatibel aussieht, und macht dann weiter. Das Decrypt-Fail-„Undo" stellt die Versionsnummer wieder her und lässt die Keys auf v2. Ein Handshake-Typ- oder abgeschnittenes Paket erreicht dieses Undo nicht einmal.
Ein einziges UDP-Datagramm vom Server-4-Tupel stoppt den Handshake eines Standard-Clients. Der Prozess läuft weiter. Kein RCE.
| ID | noch keine CVE |
| CWE | CWE-345, CWE-347, CWE-670 |
| CVSS | High: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| Produkt | MsQuic |
| Betroffen | v2.6.1 (a01333cf); bei meiner Prüfung noch auf main |
| Auth | unauthentifiziertes UDP, Quelle muss das Server-4-Tupel sein (On-Path oder IP-Spoofing des Servers plus Client-Port) |
| Lizenz | GNU Affero GPL v3.0 |
| Lab | nur 127.0.0.1 |
Während eines QUIC-v1-Handshakes ein Long-Header-Datagramm senden, das von der Server-Adresse und dem Server-Port stammt. Die Version ist QUIC v2 (RFC 9369, Wire 6b 33 43 cf). DestCidLength ist 0. Standard-Clients verwenden einen exklusiv verbundenen UDP-Socket, akzeptieren also eine DCID der Länge null. Die Bibliothek behandelt das als kompatible Versionsaushandlung vor AEAD, leitet neue Initial-Keys ab und trifft dann entweder nie das Decrypt-Fail-Revert (Handshake-Typ wird verzögert; abgeschnittenes Initial gibt zuerst FALSE zurück) oder setzt Stats.QuicVersion zurück und lässt die Keys auf v2.
Der echte Server spricht weiterhin v1. Der Client sendet mit v2-Salts erneut. Dieser Versuch erreicht nie Connected. Andere Verbindungen im selben Prozess bleiben unberührt. Dies ist ein Handshake-DoS, kein RCE.
Off-Path erfordert IP-Spoofing des Servers plus den ephemeren UDP-Port des Clients. On-Path genügt. Durch das exklusive Binding benötigt der Angreifer die CID nicht.
Ich habe zuerst die GHSA-Welle 2026 zu microsoft/msquic gelesen: Hostname-MITM (GHSA-w5f4-fx9m-m4q7, dieser Tag ist der Cherry-Pick), Path-UAF-RCE (GHSA-92f5-vc22-8j33), ACK-Underflow (GHSA-gvvw-8j96-8g5r). Diese Sinks sind in v2.6.1 geschlossen.
Dann Paket-Header, CID, Versionsaushandlung, Retry, Stateless Reset, CIBIR. Die kompatible VN nach RFC 9368 ist der Rest. QuicConnRecvHeader sieht eine Versionsabweichung, fragt, ob sie kompatibel ist, schreibt OriginalQuicVersion, setzt CompatibleVerNegotiationAttempted, weist Stats.QuicVersion zu, ruft QuicCryptoOnVersionChange auf und macht weiter. Der Kommentar besagt, dass Transportparameter später ChosenVersion prüfen werden. Diese TPs kommen nie an, wenn der Handshake nie entschlüsselt wird.
Decrypt-Fail sieht wie ein Fix aus, bis man es liest:
Connection->Stats.QuicVersion = Connection->OriginalQuicVersion;
Connection->State.CompatibleVerNegotiationAttempted = FALSE;
Kein zweiter QuicCryptoOnVersionChange. HKDF-Labels bleiben auf v2. Handshake-Typ-Pakete werden verzögert (HANDSHAKE > ReadKey=INITIAL) und RecvHeader gibt FALSE zurück, sodass dieses Undo nie ausgeführt wird.
Ich brauchte src/tools/attack nicht. Sieben Bytes genügen.
Bereits dokumentierte Irrwege: Injektion am Listen-Port des Servers (der Client ist exklusiv verbunden, er wird das nicht sehen); Weiterleitung des Initial vor dem Poison auf Loopback (der Handshake ist zuerst fertig, der Poison kommt zu spät); getchar() als Warten des Beispielservers (Docker ist keine Tastatur); flacher Clone mit leerem openssl-Submodul (das Image muss v2.6.1 holen und openssl initialisieren); irgendein Junk-Long-Header als Beweis (nicht kompatibles 11223344 ergibt trotzdem Connected - der interessante Teil ist kompatibles v2). Theater: eine CRYPTO-Payload, eine Reverse Shell, RCE. Der Zeuge ist das Handshake-Ergebnis.
cd lab
./run.sh
Der UDP-Proxy 127.0.0.1:18150 ist die Adresse, mit der sich der Client verbindet. Der echte quicsample-Server ist 127.0.0.1:18151. Beim ersten Client-Datagramm sendet der PoC ein abgeschnittenes v2-Handshake f06b3343cf0000 vom Proxy-Socket und leitet dann weiter. Binde ihn an Loopback.
control_connected=1
inject_connected=0
negative_connected=1
inject_hex=f06b3343cf0000
SUCCESS MSQUIC-COMPAT-VN-KEY-POISON-WITNESS
CONTROL ist nur weiterleitend und muss Connected sein. NEGATIVE injiziert zuerst f0112233440000 und muss trotzdem Connected sein. INJECT ist die einzige Phase, die stirbt.
QuicCryptoOnVersionChange erst aufrufen, wenn das auslösende Paket entschlüsselt ist. Bei jedem Fehler nach einem Commit (RecvHeader FALSE und Decrypt-Fail) die Version wiederherstellen und die Keys zurück zu OriginalQuicVersion ableiten. Handshake / 0-RTT als Auslöser für kompatible VN ignorieren. Nur ein entschlüsselbares Initial der neuen Version sollte umschalten.
connection.c · crypto.c · binding.c · version_neg.c