Proof-of-concept di laboratorio che riproduce CVE-2026-19553, in cui CPython ssl.SSLContext.wrap_bio() salta silenziosamente la verifica del nome host TLS quando server_hostname è None.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19553-wrap-bio
CPython ssl - Python Software Foundation
ssl.SSLContext.wrap_bio() non richiedeva che server_hostname fosse non-None quando check_hostname è impostato. SSLObject salta silenziosamente la verifica del nome host. Il programma sembra essere riuscito con check_hostname=True. Nessun ValueError. La catena di certificati viene verificata. Il nome del peer no. wrap_socket() sollevava già l'errore. asyncio SSLProtocol / start_tls / open_connection trasformano "" in None e poi chiamano wrap_bio.
Un MITM con un certificato valido per una CA per un nome host diverso completa l'handshake contro un client wrap_bio / asyncio che ha dimenticato di passare 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 |
| 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 | MITM / server TLS dell'attaccante; la vittima è un client TLS Python che usa wrap_bio o asyncio senza un nome host |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Posizionarsi sul percorso (o essere il server che il client intendeva raggiungere). Presentare un certificato di cui il client si fida per un nome che non è quello che il client intendeva. Se il client ha usato wrap_bio(..., server_hostname=None) o asyncio con server_hostname="" mentre check_hostname=True e CERT_REQUIRED, l'handshake riesce. L'identità non è mai stata verificata. wrap_socket sullo stesso contesto solleva ValueError prima che si muova qualsiasi byte. Passare un nome host non vuoto a wrap_bio rifiuta comunque una mancata corrispondenza.
Stesso prodotto, residuo affine: SNI SSLContext UAF. Lato TLS opposto. Non si compongono.
La PSF ha pubblicato CVE-2026-19553. Issue python/cpython#156793, PR 158503, commit 6dc0069a. _check_sslobject_params veniva già eseguito per wrap_socket. SSLObject._create lo saltava. NEWS: completare un handshake che verificava la catena di certificati senza verificare l'identità del peer, senza alcuna indicazione che il controllo fosse stato saltato. Dopo la patch, wrap_bio con check_hostname=True e server_hostname=None/"" solleva ValueError("check_hostname requires server_hostname"). Il backport per la 3.12 solleva invece DeprecationWarning.
Ho avviato python:3.14.7-slim-bookworm. Stessa CA del lab, due foglie: victim.lab e evil.lab. Client check_hostname=True, CERT_REQUIRED.
INJECT: wrap_bio(server_hostname=None) contro evil.lab accettato. SAN del peer evil.lab. SNI None. TLS_AES_256_GCM_SHA384. CONTROL A: wrap_socket(server_hostname=None) ha sollevato ValueError: check_hostname requires server_hostname. CONTROL B: wrap_bio(server_hostname="victim.lab") contro evil.lab ha sollevato SSLCertVerificationError per mancata corrispondenza del nome host. NEGATIVE: la stessa chiamata contro victim.lab accettata. Extra: asyncio.open_connection(..., server_hostname="") su 127.0.0.1:18510 accettato ("" diventa None, poi wrap_bio).
Le svolte sbagliate sono già state registrate: la prima bozza di start_server in asyncio aveva perso una parentesi di chiusura; la compilazione sull'host l'ha intercettata prima di compose. L'host 3.14.7 (Clang, OpenSSL 3.6.4) ha riprodotto gli stessi quattro oracoli; il record del laboratorio è il pin del container (GCC, OpenSSL 3.0.22). Teatro: una reverse shell. L'oracolo è accept-wrong-name più wrap_socket che continua a sollevare un'eccezione.
cd lab
./run.sh
Immagine python:3.14.7-slim-bookworm. Progetto Compose cve-2026-19553. Il loopback 127.0.0.1:18510 è l'extra asyncio. MemoryBIO non richiede alcuna porta.
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
Passare un server_hostname non vuoto a wrap_bio(), asyncio.create_connection() o loop.start_tls(). Aggiornare a 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0. La patch converte solo il salto silenzioso nello stesso ValueError che wrap_socket già sollevava.