Lab de preuve de concept pour CVE-2026-19445, un use-after-free de SSLContext SNI ssl de CPython où un client TLS distant libère le contexte de répartition du serveur pendant la poignée de main.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19445-sni-uaf
CPython ssl - Python Software Foundation
Un serveur TLS qui crée un SSLContext par connexion, définit sni_callback et assigne sslobj.context = other_ctx peut supprimer la dernière référence Python au contexte d'origine alors qu'OpenSSL détient encore un pointeur emprunté vers celui-ci. Le ClientHello suivant (un HelloRetryRequest TLS 1.3 suffit) consulte ce pointeur. La poignée de main se poursuit. Le processus peut planter plus tard. Les clients ne sont pas affectés. Un wrap de longue durée du socket d'écoute n'est pas affecté.
Un client TLS distant peut libérer le SSLContext de dispatch du serveur alors que le callback SNI en C pointe encore vers lui. La poignée de main se termine quand même. Fenêtre de crash et de corruption mémoire. Pas de RCE dans ce lab.
| ID | CVE-2026-19445 |
| CWE | CWE-416 |
| CVSS | Critique : 9.2 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L |
| Produit | CPython ssl |
| Affecté | < 3.12.15, 3.13.0–3.13.15, 3.14.0–3.14.7, 3.15.0a1 avant 3.15.0 |
| Auth | client TLS non authentifié, le serveur doit faire un SNI-switch et supprimer le contexte d'origine |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
Parler TLS à un serveur Python qui crée un SSLContext neuf pour la connexion, installe sni_callback, et dans ce callback assigne un contexte différent puis laisse l'original mourir. Envoyer un SNI. Forcer un second ClientHello (restreindre la courbe du serveur à P-384 pour qu'un client X25519 par défaut reçoive un HelloRetryRequest). Après le GC, l'objet Python a disparu. Le callback C tlsext_servername détient toujours le pointeur args emprunté issu de SSL_CTX_set_tlsext_servername_arg. La poignée de main se termine sur ce pin. Sous ASan, c'est un use-after-free. En production, c'est une fenêtre de crash / corruption la prochaine fois que cette mémoire est réutilisée.
Les serveurs qui wrappent le socket d'écoute une seule fois avec un contexte à durée de vie du processus conservent la référence. Ceux-là sont hors du bug. Les clients wrap_socket sont hors du bug.
Même produit, reste frère : wrap_bio saute la vérification du hostname. Côté TLS opposé. Ils ne se composent pas.
La PSF a publié CVE-2026-19445. L'issue de réservation est python/cpython#156293. La vraie carte est PR 158504 / commit 0f63c2aa. _servername_callback utilisait l'arg OpenSSL emprunté. Après que le SNI change sslobj.context et que le SSLContext d'origine soit GC'd, le second ClientHello appelle encore dedans. Le patch récupère le contexte via SSL_get_app_data → ssl->ctx et efface le callback dans context_dealloc.
J'ai exécuté les propres tests de CPython comme lab contre python:3.14.7-slim-bookworm (3.14.7 est dans la plage affectée ; 3.14.8 est le correctif). MemoryBIO, pas de TCP pour le chemin UAF.
dispatch_ctx.set_ecdh_curve("secp384r1"), client par défaut X25519. sni_callback assigne sslobj.context = leaf_ctx. Après le premier SNI, trois appels gc.collect(), weakref.ref(dispatch_ctx) est None. La poignée de main se termine quand même (TLS_AES_256_GCM_SHA384). Le HRR est dans _msg_callback (random du ServerHello à l'offset 6). sni_calls=1 : le second ClientHello n'a jamais atteint un callback Python vivant.
Le chemin secondaire (le callback switch, del ctx, lève LookupError) n'a pas avorté 3.14.7. SSLError: CALLBACK_FAILED, LookupError non levable. Négatif : contexte à durée de vie du processus wrappant un socket d'écoute sur 127.0.0.1:18500. La poignée de main fonctionne. La weakref reste vivante.
Fausses pistes déjà consignées : épingler P-384 sur la leaf ainsi que sur le dispatch, plus verrouiller le client en X25519-only, a produit NO_SUITABLE_KEY_SHARE au lieu de HRR. CPython n'épingle que le contexte de dispatch. La magie HRR est à l'offset 6 du ServerHello, pas 2. La première sonde a raté le HRR même quand il se déclenchait. dispatch_ctx est encore vivant juste après wrap_bio parce que server.context le détient. Il ne devient collectable qu'après le switch SNI. Théâtre : un reverse shell. L'oracle est dispatch_ref is None plus une poignée de main terminée.
cd lab
./run.sh
Image python:3.14.7-slim-bookworm. Projet Compose cve-2026-19445. Loopback 127.0.0.1:18500 est le wrap d'écoute négatif. Les oracles primaire/secondaire sont en mémoire MemoryBIO.
primary_dispatch_ref_after_sni_gc='none'
primary_hrr='yes'
primary_handshake='complete'
SUCCESS CVE-2026-19445 dispatch_ctx_gc=yes handshake=complete hrr=yes secondary_crash=no CVE-2026-19445-SNI-UAF-WITNESS
Conserver une référence vers chaque SSLContext qui définit sni_callback pendant toute la durée de vie du serveur. Mettre à jour vers 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0. Le patch C cesse d'utiliser l'arg OpenSSL emprunté.