Lab de preuve de concept reproduisant CVE-2026-19553, où CPython ssl.SSLContext.wrap_bio() ignore silencieusement la vérification du nom d'hôte TLS lorsque server_hostname est None.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19553-wrap-bio
CPython ssl - Python Software Foundation
ssl.SSLContext.wrap_bio() n'exigeait pas que server_hostname soit non-None lorsque check_hostname est défini. SSLObject ignore silencieusement la vérification du nom d'hôte. Le programme semble avoir réussi avec check_hostname=True. Pas de ValueError. La chaîne de certificats est vérifiée. Le nom du pair ne l'est pas. wrap_socket() levait déjà une erreur. asyncio SSLProtocol / start_tls / open_connection transforment "" en None puis appellent wrap_bio.
Un MITM avec un certificat valide par une CA pour un autre nom d'hôte complète la poignée de main contre un client wrap_bio / asyncio qui a oublié de passer server_hostname.
| ID | CVE-2026-19553 |
| CWE | CWE-297 |
| CVSS | Élevé : 7.6 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N |
| Produit | CPython ssl |
| Affecté | < 3.12.15, 3.13.0–3.13.15, 3.14.0–3.14.7, 3.15.0a1 avant 3.15.0 |
| Auth | MITM / serveur TLS attaquant ; la victime est un client TLS Python utilisant wrap_bio ou asyncio sans nom d'hôte |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
Se placer sur le chemin (ou être le serveur que le client voulait atteindre). Présenter un certificat auquel le client fait confiance pour un nom qui n'est pas celui prévu par le client. Si le client a utilisé wrap_bio(..., server_hostname=None) ou asyncio avec server_hostname="" alors que check_hostname=True et CERT_REQUIRED, la poignée de main réussit. L'identité n'a jamais été vérifiée. wrap_socket sur le même contexte lève ValueError avant que le moindre octet ne circule. Passer un nom d'hôte non vide à wrap_bio rejette toujours une incohérence.
Même produit, reste frère : SNI SSLContext UAF. Côté TLS opposé. Ils ne se composent pas.
La PSF a publié CVE-2026-19553. Issue python/cpython#156793, PR 158503, commit 6dc0069a. _check_sslobject_params s'exécutait déjà pour wrap_socket. SSLObject._create l'a ignoré. NEWS : compléter une poignée de main qui a vérifié la chaîne de certificats sans vérifier l'identité du pair, sans aucune indication que la vérification avait été ignorée. Après le correctif, wrap_bio avec check_hostname=True et server_hostname=None/"" lève ValueError("check_hostname requires server_hostname"). Le backport 3.12 lève un DeprecationWarning à la place.
J'ai déployé python:3.14.7-slim-bookworm. Même CA de lab, deux feuilles : victim.lab et evil.lab. Client check_hostname=True, CERT_REQUIRED.
INJECT : wrap_bio(server_hostname=None) contre evil.lab accepté. SAN du pair evil.lab. SNI None. TLS_AES_256_GCM_SHA384. CONTROL A : wrap_socket(server_hostname=None) a levé ValueError: check_hostname requires server_hostname. CONTROL B : wrap_bio(server_hostname="victim.lab") contre evil.lab a levé SSLCertVerificationError hostname mismatch. NEGATIVE : même appel contre victim.lab accepté. En plus : asyncio.open_connection(..., server_hostname="") sur 127.0.0.1:18510 accepté ("" devient None, puis wrap_bio).
Fausses pistes déjà consignées : le premier brouillon asyncio start_server avait perdu une parenthèse fermante ; la compilation sur l'hôte l'a détectée avant le compose. L'hôte 3.14.7 (Clang, OpenSSL 3.6.4) a reproduit les mêmes quatre oracles ; l'enregistrement du lab est l'épinglage du conteneur (GCC, OpenSSL 3.0.22). Théâtre : un reverse shell. L'oracle est accept-wrong-name plus wrap_socket qui lève toujours une erreur.
cd lab
./run.sh
Image python:3.14.7-slim-bookworm. Projet Compose cve-2026-19553. Loopback 127.0.0.1:18510 est l'extra asyncio. MemoryBIO n'a besoin d'aucun port.
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
Passer un server_hostname non vide à wrap_bio(), asyncio.create_connection(), ou loop.start_tls(). Mettre à niveau vers 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0. Le correctif ne fait que transformer l'ignorance silencieuse en la même ValueError que wrap_socket levait déjà.