
CVE-2026-19445에 대한 개념 증명(Proof-of-concept) 랩으로, 원격 TLS 클라이언트가 핸드셰이크 중에 서버의 디스패치 컨텍스트를 해제하는 CPython ssl SNI SSLContext use-after-free 취약점입니다.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19445-sni-uaf
CPython ssl - Python Software Foundation
연결마다 SSLContext를 생성하고, sni_callback을 설정한 뒤, sslobj.context = other_ctx를 할당하는 TLS 서버는 OpenSSL이 여전히 원래 컨텍스트에 대한 차용 포인터를 보유하고 있는 동안 원래 컨텍스트에 대한 마지막 Python 참조를 삭제할 수 있습니다. 다음 ClientHello(TLS 1.3 HelloRetryRequest로 충분함)가 해당 포인터를 참조합니다. 핸드셰이크는 계속됩니다. 프로세스는 나중에 충돌할 수 있습니다. 클라이언트는 영향을 받지 않습니다. 수신 소켓을 오래 유지하는 래핑은 영향을 받지 않습니다.
원격 TLS 클라이언트가 C SNI 콜백이 여전히 가리키고 있는 서버의 디스패치 SSLContext를 해제할 수 있습니다. 핸드셰이크는 여전히 완료됩니다. 충돌 및 메모리 손상 창이 존재합니다. 이 랩에서는 RCE는 없습니다.
| ID | CVE-2026-19445 |
| CWE | CWE-416 |
| CVSS | 치명적: 9.2 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L |
| 제품 | CPython ssl |
| 영향 받음 | < 3.12.15, 3.13.0-3.13.15, 3.14.0-3.14.7, 3.15.0a1 (3.15.0 이전) |
| 인증 | 인증되지 않은 TLS 클라이언트, 서버가 SNI 전환을 수행하고 원래 컨텍스트를 삭제해야 함 |
| 라이선스 | GNU Affero GPL v3.0 |
| 랩 | 127.0.0.1 전용 |
연결마다 새로운 SSLContext를 생성하고, sni_callback을 설치하며, 해당 콜백에서 다른 컨텍스트를 할당한 뒤 원래 컨텍스트가 소멸되도록 하는 Python 서버와 TLS로 통신합니다. SNI를 전송합니다. 두 번째 ClientHello를 강제합니다(서버 곡선을 P-384로 제한하여 기본 X25519 클라이언트가 HelloRetryRequest를 받도록 함). GC 이후 Python 객체는 사라집니다. C tlsext_servername 콜백은 여전히 SSL_CTX_set_tlsext_servername_arg에서 가져온 차용된 args 포인터를 보유합니다. 핸드셰이크는 이 핀에서 완료됩니다. ASan에서는 use-after-free입니다. 프로덕션에서는 해당 메모리가 다음에 재사용될 때 충돌/손상 창이 됩니다.
수신 소켓을 프로세스 수명 컨텍스트로 한 번만 래핑하는 서버는 참조를 유지합니다. 이들은 버그 범위 밖입니다. wrap_socket 클라이언트도 버그 범위 밖입니다.
같은 제품의 형제 잔여물: wrap_bio가 호스트 이름 검증을 건너뜀. 반대쪽 TLS 측입니다. 이들은 조합되지 않습니다.
PSF가 CVE-2026-19445를 게시했습니다. 예약 이슈는 python/cpython#156293입니다. 실제 지도는 PR 158504 / 커밋 0f63c2aa입니다. _servername_callback이 차용된 OpenSSL 인자를 사용했습니다. SNI가 sslobj.context를 전환하고 원래 SSLContext가 GC된 후에도 두 번째 ClientHello는 여전히 이를 호출합니다. 패치는 SSL_get_app_data -> ssl->ctx에서 컨텍스트를 조회하고 context_dealloc에서 콜백을 지웁니다.
python:3.14.7-slim-bookworm(3.14.7은 영향 범위에 포함되며, 3.14.8이 수정 버전임)을 대상으로 CPython 자체 테스트를 랩으로 실행했습니다. MemoryBIO를 사용하며 UAF 경로에는 TCP를 사용하지 않습니다.
dispatch_ctx.set_ecdh_curve("secp384r1"), 클라이언트 기본값은 X25519입니다. sni_callback이 sslobj.context = leaf_ctx를 할당합니다. 첫 번째 SNI 이후 gc.collect()를 세 번 호출하면 weakref.ref(dispatch_ctx)가 None이 됩니다. 핸드셰이크는 여전히 완료됩니다(TLS_AES_256_GCM_SHA384). HRR은 _msg_callback에 있습니다(ServerHello random은 오프셋 6). sni_calls=1: 두 번째 ClientHello는 살아 있는 Python 콜백에 도달하지 못했습니다.
보조 경로(콜백이 컨텍스트를 전환하고, del ctx를 수행하며, LookupError를 발생시킴)는 3.14.7을 중단시키지 못했습니다. SSLError: CALLBACK_FAILED, 처리되지 않은 LookupError. 부정적 사례: 127.0.0.1:18500에서 리슨 소켓을 감싸는 프로세스 수명 컨텍스트입니다. 핸드셰이크가 작동합니다. Weakref는 살아 있는 상태로 유지됩니다.
이미 기록된 잘못된 시도들: 디스패치뿐 아니라 leaf에도 P-384를 고정하고 클라이언트를 X25519 전용으로 잠그면 HRR 대신 NO_SUITABLE_KEY_SHARE가 발생했습니다. CPython은 디스패치 컨텍스트만 고정합니다. HRR 매직은 ServerHello 오프셋 2가 아니라 6에 있습니다. 첫 번째 프로브는 HRR이 발생했을 때조차 이를 놓쳤습니다. dispatch_ctx는 server.context가 이를 보유하기 때문에 wrap_bio 직후에도 여전히 살아 있습니다. 수집 가능해지는 것은 SNI 전환 이후에만입니다. 극적인 요소: 리버스 셸. 오라클은 dispatch_ref is None과 완료된 핸드셰이크입니다.
cd lab
./run.sh
Image python:3.14.7-slim-bookworm. Compose 프로젝트 cve-2026-19445. 루프백 127.0.0.1:18500은 네거티브 리슨 랩입니다. 기본/보조 오라클은 인메모리 MemoryBIO입니다.
primary_dispatch_ref_after_sni_gc='none'
primary_hrr='yes'
primary_handshake='complete'
SUCCESS CVE-2026-19445 dispatch_ctx_gc=yes handshake=complete hrr=yes secondary_crash=no CVE-2026-19445-SNI-UAF-WITNESS
서버 수명 동안 sni_callback을 설정하는 모든 SSLContext에 대한 참조를 유지하세요. 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0으로 업그레이드하세요. C 패치는 빌려온 OpenSSL 인자를 사용하지 않도록 중단합니다.