Лаборатория proof-of-concept, воспроизводящая CVE-2026-19553, где CPython ssl.SSLContext.wrap_bio() молча пропускает проверку имени хоста TLS, когда 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() не требовал, чтобы server_hostname был не-None, когда установлен check_hostname. SSLObject молча пропускает проверку имени хоста. Программа выглядит так, будто всё прошло успешно с check_hostname=True. Никакого ValueError. Цепочка сертификатов проверена. Имя узла — нет. wrap_socket() уже выбрасывал исключение. asyncio SSLProtocol / start_tls / open_connection превращают "" в None и затем вызывают wrap_bio.
MITM с CA-валидным сертификатом для другого имени хоста завершает рукопожатие с клиентом wrap_bio / asyncio, который забыл передать 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 |
| Продукт | CPython ssl |
| Затронуто | < 3.12.15, 3.13.0–3.13.15, 3.14.0–3.14.7, 3.15.0a1 до 3.15.0 |
| Аутентификация | MITM / атакующий TLS-сервер; жертва — Python TLS-клиент, использующий wrap_bio или asyncio без имени хоста |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Находиться на пути (или быть сервером, к которому клиент намеревался подключиться). Предъявить сертификат, которому клиент доверяет для имени, отличного от того, которое клиент имел в виду. Если клиент использовал wrap_bio(..., server_hostname=None) или asyncio с server_hostname="" при check_hostname=True и CERT_REQUIRED, рукопожатие завершается успешно. Идентичность никогда не проверялась. wrap_socket на том же контексте выбрасывает ValueError до передачи любых байтов. Передача непустого имени хоста в wrap_bio по-прежнему отклоняет несоответствие.
Тот же продукт, родственный остаток: SNI SSLContext UAF. Противоположная сторона TLS. Они не сочетаются.
PSF опубликовал CVE-2026-19553. Issue python/cpython#156793, PR 158503, коммит 6dc0069a. _check_sslobject_params уже выполнялся для wrap_socket. SSLObject._create его пропускал. NEWS: завершение рукопожатия, которое проверило цепочку сертификатов без проверки идентичности узла, без каких-либо признаков того, что проверка была пропущена. После патча wrap_bio с check_hostname=True и server_hostname=None/"" выбрасывает ValueError("check_hostname requires server_hostname"). Бэкпорт для 3.12 вместо этого выбрасывает DeprecationWarning.
Я развернул python:3.14.7-slim-bookworm. Тот же лабораторный CA, два листовых сертификата: victim.lab и evil.lab. Клиент с check_hostname=True, CERT_REQUIRED.
INJECT: wrap_bio(server_hostname=None) против evil.lab принят. SAN узла evil.lab. SNI None. TLS_AES_256_GCM_SHA384. CONTROL A: wrap_socket(server_hostname=None) выбросил ValueError: check_hostname requires server_hostname. CONTROL B: wrap_bio(server_hostname="victim.lab") против evil.lab выбросил SSLCertVerificationError hostname mismatch. NEGATIVE: тот же вызов против victim.lab принят. Дополнительно: asyncio.open_connection(..., server_hostname="") на 127.0.0.1:18510 принят ("" становится None, затем wrap_bio).
Ошибочные пути уже зафиксированы: первый черновик asyncio start_server потерял закрывающую скобку; компиляция на хосте поймала это до compose. Хост 3.14.7 (Clang, OpenSSL 3.6.4) воспроизвёл те же четыре оракула; лабораторная запись — это закрепление контейнера (GCC, OpenSSL 3.0.22). Театр: reverse shell. Оракул — это accept-wrong-name плюс wrap_socket, всё ещё выбрасывающий исключение.
cd lab
./run.sh
Образ python:3.14.7-slim-bookworm. Compose-проект cve-2026-19553. Loopback 127.0.0.1:18510 — это дополнение для asyncio. MemoryBIO не требует порта.
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
Передавайте непустой server_hostname в wrap_bio(), asyncio.create_connection() или loop.start_tls(). Обновитесь до 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0. Патч лишь превращает молчаливый пропуск в тот же ValueError, который wrap_socket уже выбрасывал.