CVE-2026-19553을 재현하는 개념 증명(Proof-of-concept) 랩으로, CPython의 ssl.SSLContext.wrap_bio()가 server_hostname이 None일 때 TLS 호스트 이름 검증을 조용히 건너뛰는 취약점을 다룹니다.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19553-wrap-bio
CPython ssl - Python Software Foundation
ssl.SSLContext.wrap_bio()는 check_hostname이 설정된 경우 server_hostname이 None이 아닐 것을 요구하지 않았다. SSLObject는 호스트 이름 검증을 조용히 건너뛴다. 프로그램은 check_hostname=True로 성공한 것처럼 보인다. ValueError는 없다. 인증서 체인은 검증된다. 피어의 이름은 검증되지 않는다. wrap_socket()은 이미 예외를 발생시켰다. asyncio SSLProtocol / start_tls / open_connection은 ""를 None으로 바꾼 뒤 wrap_bio를 호출한다.
다른 호스트 이름에 대한 CA 유효 인증서를 가진 MITM이 server_hostname 전달을 잊은 wrap_bio / asyncio 클라이언트와의 핸드셰이크를 완료한다.
| 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 |
| Product | CPython ssl |
| Affected | < 3.12.15, 3.13.0–3.13.15, 3.14.0–3.14.7, 3.15.0a1 before 3.15.0 |
| Auth | MITM / 공격자 TLS 서버; 피해자는 호스트 이름 없이 wrap_bio 또는 asyncio를 사용하는 Python TLS 클라이언트 |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 전용 |
경로 상에 위치하거나(또는 클라이언트가 도달하려던 서버가 되어) 클라이언트가 신뢰하는 인증서를 클라이언트가 의도한 이름이 아닌 이름으로 제시한다. 클라이언트가 check_hostname=True와 CERT_REQUIRED 상태에서 wrap_bio(..., server_hostname=None) 또는 server_hostname=""로 asyncio를 사용했다면 핸드셰이크가 성공한다. 신원은 결코 확인되지 않았다. 동일한 컨텍스트에서 wrap_socket은 바이트가 오가기 전에 ValueError를 발생시킨다. wrap_bio에 비어 있지 않은 호스트 이름을 전달하면 여전히 불일치를 거부한다.
동일 제품, 형제 잔재: SNI SSLContext UAF. 반대편 TLS 측면이다. 이 둘은 조합되지 않는다.
PSF가 CVE-2026-19553을 게시했다. 이슈 python/cpython#156793, PR 158503, 커밋 6dc0069a. _check_sslobject_params는 이미 wrap_socket에 대해 실행되고 있었다. SSLObject._create는 이를 건너뛰었다. NEWS: 검사가 건너뛰어졌다는 표시 없이 인증서 체인은 검증하면서 피어의 신원은 검증하지 않은 채 핸드셰이크를 완료하는 문제. 패치 이후, check_hostname=True와 server_hostname=None/""를 사용한 wrap_bio는 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: evil.lab에 대한 wrap_bio(server_hostname=None)이 수락됨. 피어 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: evil.lab에 대한 wrap_bio(server_hostname="victim.lab")이 SSLCertVerificationError 호스트 이름 불일치를 발생시킴. NEGATIVE: victim.lab에 대한 동일 호출이 수락됨. 추가: 127.0.0.1:18510에서 asyncio.open_connection(..., server_hostname="")이 수락됨 (""가 None이 된 후 wrap_bio).
이미 기록된 잘못된 시도: 첫 asyncio start_server 초안에서 닫는 괄호가 누락됨; 호스트 컴파일이 compose 전에 이를 잡아냄. 호스트 3.14.7 (Clang, OpenSSL 3.6.4)에서 동일한 네 가지 오라클이 재현됨; 랩 기록은 컨테이너 고정(GCC, OpenSSL 3.0.22)이다. 극적인 요소: 리버스 셸. 오라클은 잘못된 이름 수락과 wrap_socket이 여전히 예외를 발생시키는 것이다.
cd lab
./run.sh
이미지 python:3.14.7-slim-bookworm. Compose 프로젝트 cve-2026-19553. 루프백 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
wrap_bio(), asyncio.create_connection(), 또는 loop.start_tls()에 비어 있지 않은 server_hostname을 전달하라. 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0으로 업그레이드하라. 패치는 조용한 건너뛰기를 wrap_socket이 이미 발생시키던 동일한 ValueError로 바꿀 뿐이다.