
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로 충분)가 그 포인터를 참조합니다. 핸드셰이크는 계속됩니다. 프로세스는 나중에 충돌할 수 있습니다. 클라이언트는 영향을 받지 않습니다. 리스닝 소켓을 오래 유지하는 wrap도 영향을 받지 않습니다.
원격 TLS 클라이언트가 서버의 디스패치 SSLContext를 해제할 수 있으며, C SNI 콜백은 여전히 그것을 가리킵니다. 핸드셰이크는 여전히 완료됩니다. 충돌 및 메모리 손상 윈도우. 이 랩에서는 RCE는 없습니다.
| ID | CVE-2026-19445 |
| CWE | CWE-416 |
| CVSS | Critical: 9.2 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L |
| 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 | 인증되지 않은 TLS 클라이언트, 서버가 SNI 전환을 하고 원래 컨텍스트를 놓아야 함 |
| License | GNU Affero GPL v3.0 |
| Lab | 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하는 서버는 참조를 유지합니다. 그들은 이 버그 밖에 있습니다. wrap_socket 클라이언트도 이 버그 밖에 있습니다.
같은 제품, 형제로 남은 것: wrap_bio가 호스트 이름 검증을 건너뜀. 반대쪽 TLS 측면입니다. 이들은 조합되지 않습니다.
PSF가 CVE-2026-19445를 게시했습니다. 예약 이슈는 python/cpython#156293입니다. 실제 지도는 PR 158504 / 커밋 0f63c2aa입니다. _servername_callback이 빌린 OpenSSL arg를 사용했습니다. SNI가 sslobj.context를 전환하고 원래 SSLContext가 GC된 후, 두 번째 ClientHello는 여전히 그것을 호출합니다. 패치는 SSL_get_app_data → ssl->ctx에서 컨텍스트를 조회하고 context_dealloc에서 콜백을 지웁니다.
저는 CPython 자체 테스트를 python:3.14.7-slim-bookworm에 대한 랩으로 실행했습니다(3.14.7은 영향을 받는 범위에 있고, 3.14.8이 수정입니다). 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, unraisable LookupError. 부정: 127.0.0.1:18500에서 리슨 소켓을 wrap하는 프로세스 수명 컨텍스트. 핸드셰이크가 작동합니다. Weakref는 살아 있습니다.
이미 기록된 잘못된 방향: leaf와 dispatch 모두에 P-384를 고정하고 클라이언트를 X25519 전용으로 잠그면 HRR 대신 NO_SUITABLE_KEY_SHARE가 발생했습니다. CPython은 디스패치 컨텍스트만 고정합니다. HRR 매직은 ServerHello 오프셋 2가 아니라 6에 있습니다. 첫 프로브는 HRR이 발생했을 때도 HRR을 놓쳤습니다. dispatch_ctx는 wrap_bio 직후에도 여전히 살아 있습니다. 왜냐하면 server.context가 그것을 들고 있기 때문입니다. SNI 전환 후에만 수집 가능해집니다. 극적인 것: 리버스 셸. 오라클은 dispatch_ref is None과 완료된 핸드셰이크입니다.
cd lab
./run.sh
이미지 python:3.14.7-slim-bookworm. Compose 프로젝트 cve-2026-19445. 루프백 127.0.0.1:18500은 부정 리슨 wrap입니다. 기본/보조 오라클은 인메모리 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 arg 사용을 중단합니다.