
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 aún 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 se completa igualmente. Ventana de fallo y corrupción de memoria. Sin RCE en este laboratorio.
| ID | CVE-2026-19445 |
| CWE | CWE-416 |
| CVSS | Crítica: 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 cambiar por SNI 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 un HelloRetryRequest). Tras el GC, el objeto Python desaparece. El callback en C tlsext_servername todavía tiene el puntero prestado args de SSL_CTX_set_tlsext_servername_arg. El handshake se completa con 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 conservan la referencia. Quedan fuera del bug. Los clientes wrap_socket quedan fuera del bug.
Mismo producto, residuo hermano: wrap_bio omite la verificación de nombre de host. 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 argumento 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 a é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 del UAF.
dispatch_ctx.set_ecdh_curve("secp384r1"), el cliente usa X25519 por defecto. 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 del ServerHello en el offset 6). sni_calls=1: el segundo ClientHello nunca llegó a un callback de Python activo.
La ruta secundaria (el callback cambia, del ctx, lanza LookupError) no abortó 3.14.7. SSLError: CALLBACK_FAILED, LookupError no capturable. Negativo: contexto de duración del proceso que envuelve un socket de escucha en 127.0.0.1:18500. El handshake funciona. El weakref sigue vivo.
Errores ya registrados: fijar P-384 tanto en la hoja 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 del 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. Solo se vuelve recolectable tras el 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 envoltorio de escucha negativo. Los oráculos primario/secundario son 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
Mantén una referencia a cada SSLContext que establece sni_callback durante toda la vida útil del servidor. Actualiza a 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0. El parche en C deja de usar el argumento prestado de OpenSSL.