Prueba de concepto de laboratorio que reproduce CVE-2026-19553, donde CPython ssl.SSLContext.wrap_bio() omite silenciosamente la verificación del nombre de host TLS cuando server_hostname es None.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19553-wrap-bio
CPython ssl - Python Software Foundation
ssl.SSLContext.wrap_bio() no requería que server_hostname fuera distinto de None cuando check_hostname está establecido. SSLObject omite silenciosamente la verificación del nombre de host. El programa parece haber tenido éxito con check_hostname=True. Sin ValueError. La cadena de certificados se verifica. El nombre del par no. wrap_socket() ya lanzaba la excepción. asyncio SSLProtocol / start_tls / open_connection convierten "" en None y luego llaman a wrap_bio.
Un MITM con un certificado válido por una CA para un nombre de host diferente completa el handshake contra un cliente wrap_bio / asyncio que olvidó pasar server_hostname.
| ID | CVE-2026-19553 |
| CWE | CWE-297 |
| CVSS | Alto: 7.6 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N |
| Producto | CPython ssl |
| Afectado | < 3.12.15, 3.13.0–3.13.15, 3.14.0–3.14.7, 3.15.0a1 anterior a 3.15.0 |
| Auth | MITM / servidor TLS atacante; la víctima es un cliente TLS de Python que usa wrap_bio o asyncio sin un nombre de host |
| Licencia | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Situarse en la ruta (o ser el servidor al que el cliente pretendía llegar). Presentar un certificado en el que el cliente confía para un nombre que no es el que el cliente pretendía. Si el cliente usó wrap_bio(..., server_hostname=None) o asyncio con server_hostname="" mientras check_hostname=True y CERT_REQUIRED, el handshake tiene éxito. La identidad nunca se verificó. wrap_socket sobre el mismo contexto lanza ValueError antes de que se mueva ningún byte. Pasar un nombre de host no vacío a wrap_bio sigue rechazando una discrepancia.
Mismo producto, residuo hermano: SNI SSLContext UAF. Lado TLS opuesto. No se componen.
La PSF publicó CVE-2026-19553. Issue python/cpython#156793, PR 158503, commit 6dc0069a. _check_sslobject_params ya se ejecutaba para wrap_socket. SSLObject._create lo omitía. NEWS: completar un handshake que verificó la cadena de certificados sin verificar la identidad del par, sin ninguna indicación de que la comprobación se había omitido. Tras el parche, wrap_bio con check_hostname=True y server_hostname=None/"" lanza ValueError("check_hostname requires server_hostname"). El backport a 3.12 lanza DeprecationWarning en su lugar.
Levanté python:3.14.7-slim-bookworm. Misma CA de laboratorio, dos hojas: victim.lab y evil.lab. Cliente check_hostname=True, CERT_REQUIRED.
INJECT: wrap_bio(server_hostname=None) contra evil.lab aceptado. SAN del par evil.lab. SNI None. TLS_AES_256_GCM_SHA384. CONTROL A: wrap_socket(server_hostname=None) lanzó ValueError: check_hostname requires server_hostname. CONTROL B: wrap_bio(server_hostname="victim.lab") contra evil.lab lanzó SSLCertVerificationError por discrepancia de nombre de host. NEGATIVE: la misma llamada contra victim.lab aceptada. Extra: asyncio.open_connection(..., server_hostname="") en 127.0.0.1:18510 aceptado ("" se convierte en None, luego wrap_bio).
Falsos caminos ya registrados: el primer borrador de start_server de asyncio omitió un paréntesis de cierre; la compilación en el host lo detectó antes del compose. El host 3.14.7 (Clang, OpenSSL 3.6.4) reprodujo los mismos cuatro oráculos; el registro del laboratorio es el pin del contenedor (GCC, OpenSSL 3.0.22). Teatro: una reverse shell. El oráculo es aceptar-nombre-incorrecto más wrap_socket que sigue lanzando la excepción.
cd lab
./run.sh
Imagen python:3.14.7-slim-bookworm. Proyecto de compose cve-2026-19553. Loopback 127.0.0.1:18510 es el extra de asyncio. MemoryBIO no necesita puerto.
INJECT wrap_bio_none vs evil.lab: accept peer=evil.lab
CONTROL_A wrap_socket_none: ValueError:check_hostname requires server_hostname
CONTROL_B wrap_bio_name='victim.lab' vs evil.lab: mismatch-reject
SUCCESS CVE-2026-19553 wrap_bio_none=accept wrap_socket_none=ValueError wrap_bio_name=mismatch-reject CVE-2026-19553-WRAPBIO-HOST-WITNESS
Pasar un server_hostname no vacío a wrap_bio(), asyncio.create_connection() o loop.start_tls(). Actualizar a 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0. El parche solo convierte la omisión silenciosa en el mismo ValueError que wrap_socket ya lanzaba.