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 è verificata. Il nome del peer no. wrap_socket() sollevava già l'eccezione. asyncio SSLProtocol / start_tls / open_connection trasformano "" in None e poi chiamano wrap_bio.
Un MITM con un certificato valido per una CA ma per un hostname 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 | High: 7.6 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N |
| Prodotto | CPython ssl |
| Affected | < 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 hostname |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Mettersi 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 hostname 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 era 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 3.12 solleva invece un 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 hostname mismatch. NEGATIVE: stessa chiamata contro victim.lab accettata. Extra: asyncio.open_connection(..., server_hostname="") su 127.0.0.1:18510 accettato ("" diventa None, poi wrap_bio).
Vicoli ciechi già registrati: la prima bozza di start_server asyncio ha perso una parentesi di chiusura; la compilazione sull'host l'ha intercettata prima del compose. L'host 3.14.7 (Clang, OpenSSL 3.6.4) ha riprodotto gli stessi quattro oracoli; il record del lab è il pin del container (GCC, OpenSSL 3.0.22). Teatro: una reverse shell. L'oracolo è accept-wrong-name più wrap_socket che solleva ancora.
cd lab
./run.sh
Immagine python:3.14.7-slim-bookworm. Progetto compose cve-2026-19553. Loopback 127.0.0.1:18510 è l'extra asyncio. MemoryBIO non necessita di porte.
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 trasforma solo il salto silenzioso nello stesso ValueError che wrap_socket sollevava già.