
Prueba de concepto de laboratorio para CVE-2026-19445, un use-after-free de CPython ssl SNI SSLContext donde un cliente TLS remoto libera el contexto de despacho del servidor durante el handshake.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19445-sni-uaf
CPython ssl - Python Software Foundation
Un servidor TLS que crea un SSLContext por conexión, establece sni_callback y asigna sslobj.context = other_ctx puede eliminar la última referencia de Python al contexto original mientras OpenSSL todavía mantiene un puntero prestado a él. El siguiente ClientHello (basta con un HelloRetryRequest de TLS 1.3) consulta ese puntero. El handshake continúa. El proceso puede fallar más tarde. Los clientes no se ven afectados. Un wrap de larga duración del socket de escucha no se ve afectado.
Un cliente TLS remoto puede liberar el SSLContext de despacho del servidor mientras el callback SNI en C todavía apunta a él. El handshake aún se completa. Ventana de fallo y corrupción de memoria. Sin RCE en este laboratorio.
| ID | CVE-2026-19445 |
| CWE | CWE-416 |
| CVSS | Crítico: 9.2 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L |
| Producto | CPython ssl |
| Afectados | < 3.12.15, 3.13.0–3.13.15, 3.14.0–3.14.7, 3.15.0a1 antes de 3.15.0 |
| Autenticación | cliente TLS no autenticado, el servidor debe hacer SNI-switch y descartar el contexto original |
| Licencia | GNU Affero GPL v3.0 |
| Laboratorio | solo 127.0.0.1 |
Hablar TLS con un servidor Python que crea un SSLContext nuevo para la conexión, instala sni_callback y en ese callback asigna un contexto diferente y luego deja morir el original. Enviar SNI. Forzar un segundo ClientHello (restringir la curva del servidor a P-384 para que un cliente X25519 por defecto reciba HelloRetryRequest). Tras el GC, el objeto Python desaparece. El callback C tlsext_servername todavía tiene el puntero prestado args de SSL_CTX_set_tlsext_servername_arg. El handshake se completa en este pin. Bajo ASan eso es un use-after-free. En producción es una ventana de fallo / corrupción la próxima vez que se reutilice esa memoria.
Los servidores que envuelven el socket de escucha una vez con un contexto de vida útil del proceso mantienen la referencia. Esos quedan fuera del bug. Los clientes wrap_socket quedan fuera del bug.
Mismo producto, residuo hermano: wrap_bio omite la verificación de hostname. Lado TLS opuesto. No se componen.
La PSF publicó CVE-2026-19445. El issue de reserva es python/cpython#156293. El mapa real es PR 158504 / commit 0f63c2aa. _servername_callback usaba el arg prestado de OpenSSL. Después de que SNI cambia sslobj.context y el SSLContext original es recolectado por el GC, el segundo ClientHello todavía llama hacia él. El parche busca el contexto desde SSL_get_app_data → ssl->ctx y limpia el callback en context_dealloc.
Ejecuté las propias pruebas de CPython como laboratorio contra python:3.14.7-slim-bookworm (3.14.7 está en el rango afectado; 3.14.8 es la corrección). MemoryBIO, sin TCP para la ruta UAF.
dispatch_ctx.set_ecdh_curve("secp384r1"), cliente por defecto X25519. sni_callback asigna sslobj.context = leaf_ctx. Tras el primer SNI, tres llamadas a gc.collect(), weakref.ref(dispatch_ctx) es None. El handshake aún se completa (TLS_AES_256_GCM_SHA384). El HRR está en _msg_callback (random de ServerHello en el offset 6). sni_calls=1: el segundo ClientHello nunca llegó a un callback Python vivo.
La ruta secundaria (el callback cambia, del ctx, lanza LookupError) no abortó 3.14.7. SSLError: CALLBACK_FAILED, LookupError no lanzable. Negativo: contexto de vida útil del proceso envolviendo un socket de escucha en 127.0.0.1:18500. El handshake funciona. El weakref sigue vivo.
Giros equivocados ya registrados: fijar P-384 tanto en el leaf como en el dispatch, además de bloquear el cliente a solo X25519, produjo NO_SUITABLE_KEY_SHARE en lugar de HRR. CPython solo fija el contexto de dispatch. La magia de HRR está en el offset 6 del ServerHello, no en el 2. La primera sonda no detectó el HRR incluso cuando se disparó. dispatch_ctx sigue vivo justo después de wrap_bio porque server.context lo mantiene. Se vuelve recolectable solo después del cambio de SNI. Teatro: una reverse shell. El oráculo es dispatch_ref is None más un handshake completado.
cd lab
./run.sh
Imagen python:3.14.7-slim-bookworm. Proyecto de Compose cve-2026-19445. El loopback 127.0.0.1:18500 es el wrap de escucha negativo. Los oráculos primario/secundario están en MemoryBIO en 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
Mantener una referencia a cada SSLContext que establece sni_callback durante la vida útil del servidor. Actualizar a 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0. El parche en C deja de usar el arg prestado de OpenSSL.