
Proof-of-concept lab per CVE-2026-19445, un caso di use-after-free in CPython ssl SNI SSLContext in cui un client TLS remoto libera il contesto di dispatch del server durante l'handshake.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19445-sni-uaf
CPython ssl - Python Software Foundation
Un server TLS che crea un SSLContext per connessione, imposta sni_callback e assegna sslobj.context = other_ctx può eliminare l'ultimo riferimento Python al contesto originale mentre OpenSSL ne detiene ancora un puntatore preso in prestito. Il successivo ClientHello (basta un HelloRetryRequest TLS 1.3) consulta quel puntatore. L'handshake prosegue. Il processo può andare in crash in seguito. I client non sono interessati. Un wrap di lunga durata del socket in ascolto non è interessato.
Un client TLS remoto può liberare l'SSLContext di dispatch del server mentre la callback C SNI punta ancora ad esso. L'handshake si completa comunque. Finestra di crash e corruzione di memoria. Nessun RCE in questo lab.
| ID | CVE-2026-19445 |
| CWE | CWE-416 |
| CVSS | Critico: 9.2 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L |
| Prodotto | CPython ssl |
| Affetto | < 3.12.15, 3.13.0–3.13.15, 3.14.0–3.14.7, 3.15.0a1 prima di 3.15.0 |
| Auth | client TLS non autenticato, il server deve fare SNI-switch e rilasciare il contesto originale |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Parlare TLS con un server Python che crea un SSLContext nuovo per la connessione, installa sni_callback e in quella callback assegna un contesto diverso per poi lasciar morire quello originale. Inviare SNI. Forzare un secondo ClientHello (limitare la curva del server a P-384 così un client X25519 predefinito riceve un HelloRetryRequest). Dopo la GC, l'oggetto Python è sparito. La callback C tlsext_servername ha ancora il puntatore args preso in prestito da SSL_CTX_set_tlsext_servername_arg. L'handshake si completa su questo pin. Sotto ASan si tratta di un use-after-free. In produzione è una finestra di crash / corruzione la prossima volta che quella memoria viene riutilizzata.
I server che fanno il wrap del socket in ascolto una sola volta con un contesto dalla vita pari a quella del processo mantengono il riferimento. Questi sono fuori dal bug. I client wrap_socket sono fuori dal bug.
Stesso prodotto, residuo affine: wrap_bio salta la verifica dell'hostname. Lato TLS opposto. Non si compongono.
La PSF ha pubblicato CVE-2026-19445. La issue di prenotazione è python/cpython#156293. La mappa reale è la PR 158504 / commit 0f63c2aa. _servername_callback usava l'arg OpenSSL preso in prestito. Dopo che SNI cambia sslobj.context e l'SSLContext originale viene raccolto dalla GC, il secondo ClientHello chiama ancora dentro di esso. La patch recupera il contesto da SSL_get_app_data → ssl->ctx e azzera la callback in context_dealloc.
Ho eseguito i test di CPython stesso come lab contro python:3.14.7-slim-bookworm (3.14.7 è nel range affetto; 3.14.8 è la correzione). MemoryBIO, nessun TCP per il percorso UAF.
dispatch_ctx.set_ecdh_curve("secp384r1"), client predefinito X25519. sni_callback assegna sslobj.context = leaf_ctx. Dopo il primo SNI, tre chiamate gc.collect(), weakref.ref(dispatch_ctx) è None. L'handshake si completa comunque (TLS_AES_256_GCM_SHA384). L'HRR è in _msg_callback (random del ServerHello all'offset 6). sni_calls=1: il secondo ClientHello non ha mai raggiunto una callback Python viva.
Il percorso secondario (la callback cambia, del ctx, solleva LookupError) non ha abortito 3.14.7. SSLError: CALLBACK_FAILED, LookupError non sollevabile. Negativo: contesto dalla vita pari a quella del processo che fa il wrap di un socket in ascolto su 127.0.0.1:18500. L'handshake funziona. Il weakref resta vivo.
Vicoli ciechi già registrati: fissare P-384 sia sulla leaf che sul dispatch, più bloccare il client a solo X25519, ha prodotto NO_SUITABLE_KEY_SHARE invece di HRR. CPython fissa solo il contesto di dispatch. La magia HRR è all'offset 6 del ServerHello, non 2. La prima sonda ha mancato l'HRR anche quando è scattato. dispatch_ctx è ancora vivo subito dopo wrap_bio perché server.context lo tiene. Diventa collezionabile solo dopo lo switch SNI. Teatro: una reverse shell. L'oracolo è dispatch_ref is None più un handshake completato.
cd lab
./run.sh
Immagine python:3.14.7-slim-bookworm. Progetto Compose cve-2026-19445. Il loopback 127.0.0.1:18500 è il wrap di ascolto negativo. Gli oracoli primario/secondario sono MemoryBIO in memoria.
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
Mantenere un riferimento a ogni SSLContext che imposta sni_callback per tutta la vita del server. Aggiornare a 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0. La patch C smette di usare l'arg OpenSSL preso in prestito.