Laboratório de prova de conceito que reproduz o CVE-2026-19553, onde o CPython ssl.SSLContext.wrap_bio() ignora silenciosamente a verificação do nome do 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() não exigia que server_hostname fosse não-None quando check_hostname está definido. SSLObject ignora silenciosamente a verificação de hostname. O programa parece ter tido sucesso com check_hostname=True. Nenhum ValueError. A cadeia de certificados é verificada. O nome do par não é. wrap_socket() já lançava erro. asyncio SSLProtocol / start_tls / open_connection transformam "" em None e então chamam wrap_bio.
Um MITM com um certificado válido por CA para um hostname diferente completa o handshake contra um cliente wrap_bio / asyncio que esqueceu de passar 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 |
| Produto | CPython ssl |
| Afetado | < 3.12.15, 3.13.0–3.13.15, 3.14.0–3.14.7, 3.15.0a1 antes de 3.15.0 |
| Auth | MITM / servidor TLS atacante; a vítima é um cliente TLS Python usando wrap_bio ou asyncio sem um hostname |
| Licença | GNU Affero GPL v3.0 |
| Lab | Apenas 127.0.0.1 |
Posicionar-se no caminho (ou ser o servidor que o cliente pretendia alcançar). Apresentar um certificado em que o cliente confia para um nome que não é o pretendido pelo cliente. Se o cliente usou wrap_bio(..., server_hostname=None) ou asyncio com server_hostname="" enquanto check_hostname=True e CERT_REQUIRED, o handshake é bem-sucedido. A identidade nunca foi verificada. wrap_socket no mesmo contexto lança ValueError antes de qualquer byte trafegar. Passar um hostname não vazio para wrap_bio ainda rejeita uma incompatibilidade.
Mesmo produto, resquício irmão: SNI SSLContext UAF. Lado TLS oposto. Eles não se compõem.
A PSF publicou CVE-2026-19553. Issue python/cpython#156793, PR 158503, commit 6dc0069a. _check_sslobject_params já era executado para wrap_socket. SSLObject._create o ignorava. NEWS: completar um handshake que verificou a cadeia de certificados sem verificar a identidade do par, sem qualquer indicação de que a verificação havia sido ignorada. Após o patch, wrap_bio com check_hostname=True e server_hostname=None/"" lança ValueError("check_hostname requires server_hostname"). O backport para 3.12 lança DeprecationWarning em vez disso.
Eu montei python:3.14.7-slim-bookworm. Mesma CA de lab, duas folhas: victim.lab e evil.lab. Cliente check_hostname=True, CERT_REQUIRED.
INJECT: wrap_bio(server_hostname=None) contra evil.lab aceito. SAN do par evil.lab. SNI None. TLS_AES_256_GCM_SHA384. CONTROL A: wrap_socket(server_hostname=None) lançou ValueError: check_hostname requires server_hostname. CONTROL B: wrap_bio(server_hostname="victim.lab") contra evil.lab lançou SSLCertVerificationError hostname mismatch. NEGATIVE: mesma chamada contra victim.lab aceita. Extra: asyncio.open_connection(..., server_hostname="") em 127.0.0.1:18510 aceito ("" torna-se None, então wrap_bio).
Caminhos errados já registrados: o primeiro rascunho de start_server do asyncio perdeu um parêntese de fechamento; a compilação no host o detectou antes do compose. O host 3.14.7 (Clang, OpenSSL 3.6.4) reproduziu os mesmos quatro oráculos; o registro do lab é o pin do container (GCC, OpenSSL 3.0.22). Teatro: um reverse shell. O oráculo é accept-wrong-name mais wrap_socket ainda lançando erro.
cd lab
./run.sh
Imagem python:3.14.7-slim-bookworm. Projeto Compose cve-2026-19553. Loopback 127.0.0.1:18510 é o extra do asyncio. MemoryBIO não precisa de 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
Passe um server_hostname não vazio para wrap_bio(), asyncio.create_connection() ou loop.start_tls(). Atualize para 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0. O patch apenas transforma a omissão silenciosa no mesmo ValueError que wrap_socket já lançava.