CVE-2026-42945에 대한 전체 연구 저장소: 힙 버퍼 오버플로우 분석, RCE 익스플로잇(힙 스프레이 + Feng Shui), 탐지 스크립트, NGINX rewrite 모듈 취약점에 대한 패치 가이드 포함.
NGINX ngx_http_rewrite_module의 힙 버퍼 오버플로우
| Metric | Value |
|---|---|
| CVSS v4.0 | 9.2 (위험) |
| CVSS v3.1 | 8.1 (높음) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | 122 — 힙 기반 버퍼 오버플로우 |
| 도입됨 | 2008년 6월 — v0.6.27 |
| 발견됨 | 2026년 4월 — DepthFirst Research |
| 수정됨 | 2026년 5월 13일 — v1.30.1, v1.31.0 |
| CVE 게시됨 | 2026년 5월 21일 |
| 수명 | 약 18년 (미탐지) |
| 수정 커밋 | 524977e7c534e87e5b55739fa74601c9f1102686 |
인증되지 않은 원격 공격자가 특정 rewrite + set/if/rewrite 구성 패턴을 가진 서버에 조작된 HTTP 요청을 전송하여 NGINX 워커 프로세스에서 결정론적 힙 버퍼 오버플로우를 트리거할 수 있습니다. 오버플로우는 힙 메타데이터(ngx_pool_cleanup_t 포인터)를 손상시켜 힙 스프레이 및 풍수 기법을 통해 **원격 코드 실행(RCE)**을 가능하게 합니다.
server { listen 19321;
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
}
**Key requirements:**
- A `rewrite` directive whose replacement contains `?` (query-string separator)
- A subsequent `set`, `if`, or `rewrite` directive that references an **unnamed PCRE capture** (`$1`, `$2`, etc.)
- The `?` in the rewrite replacement triggers `ngx_http_script_start_args_code` which sets `e->is_args = 1`
### 공격자가 달성할 수 있는 것
| 능력 | 설명 |
|-----------|-------------|
| **서비스 거부(DoS)** | 작업자 프로세스를 결정적으로 충돌시켜 재시작 루프를 유발 (ASLR과 무관하게 작동) |
| **원격 코드 실행(RCE)** | ASLR이 비활성화된 경우(또는 부분 덮어쓰기로 우회 시) nginx 사용자 권한으로 완전한 RCE 달성 |
| **데이터 유출** | 메모리 읽기 프리미티브를 통해 작업자 힙에서 민감한 데이터 추출 |
| **지속성** | 작업자 프로세스 메모리에서 코드 실행을 통해 백도어 설치 |
---
## 2. 근본 원인 분석
### 두 패스 스크립트 엔진
NGINX의 `ngx_http_rewrite_module`은 `src/http/ngx_http_script.c`에서 **두 패스 스크립트 엔진**을 사용합니다.
1. **길이 패스** (`ngx_http_script_run`): 모든 스크립트 코드를 순회하며 필요한 총 버퍼 크기를 계산합니다. 길이를 `le.ip`와 `le.pos`에 기록합니다.
2. **복사 패스** (`ngx_http_script_copy_len`/`_code`): 다시 순회하며 미리 할당된 버퍼(`e->ip`, `e->pos`)에 실제 바이트를 기록합니다.
각 스크립트 코드에는 각 패스에 대한 두 개의 핸들러가 있습니다. 예를 들어:
- `ngx_http_script_copy_len` → `ngx_http_script_copy_code`
- `ngx_http_script_start_args_len` → `ngx_http_script_start_args_code`
### `is_args` 플래그
**엔진 구조체**(`ngx_http_script_engine_t`)의 `e->is_args` 플래그는 복사 패스가 특정 문자를 처리하는 방식을 제어합니다.```c
typedef struct {
u_char *ip;
u_char *pos;
ngx_http_variable_value_t *sp;
ngx_str_t buf;
int flushed;
unsigned is_args:1; // <-- THE BUG
unsigned ncaptures:1;
ngx_uint_t captures_size;
// ...
} ngx_http_script_engine_t;
When e->is_args = 1일 때, $N 캡처 참조에 대한 복사 코드는 NGX_ESCAPE_ARGS와 함께 ngx_escape_uri()를 호출하며, 이는 다음을 확장합니다:
+ → %2B (1바이트 → 3바이트, +200%)% → %25 (1바이트 → 3바이트, +200%)& → %26 (1바이트 → 3바이트, +200%)취약한 패턴에 대한 실행 흐름:``` rewrite ^/api/(.*)$ /internal?migrated=true;
1. **재작성 평가** 중에 엔진은 대체 문자열에서 `?`를 만나 `ngx_http_script_start_args_code`를 트리거하여 `e->is_args = 1`을 설정합니다.
2. 재작성은 요청 URI를 수정한 후 다음 지시어로 계속 진행됩니다.
3. **`e->is_args`는 절대 지워지지 않습니다**.
그 다음:```
set $original_endpoint $1;
le)이 길이 패스를 위해 생성됩니다: ```c
ngx_memzero(&le, sizeof(ngx_http_script_engine_t));
이는 올바르게 le.is_args = 0으로 설정하여, 길이 전달이 원시, 이스케이프되지 않은 캡처 길이를 반환하게 합니다.
e->is_args = 1로 설정된 메인 엔진 e를 재사용합니다. 복사 전달은 URI 이스케이프를 적용하여, 원시 길이에 맞게 크기가 지정된 버퍼 내부에서 이스케이프 가능한 각 문자를 1바이트에서 3바이트로 확장합니다 — 힙 오버플로우.Pass 1 (Length — sub-engine le): le.is_args = 0 capture $1 = "A+++++B" → length = 7
Buffer allocated: 7 bytes
Pass 2 (Copy — main engine e): e.is_args = 1 ← LEAKED from rewrite capture $1 = "A+++++B" ngx_escape_uri("A+++++B", NGX_ESCAPE_ARGS): A → A (1 byte) + → %2B (3 bytes) ← EXPANSION + → %2B (3 bytes) + → %2B (3 bytes) + → %2B (3 bytes) + → %2B (3 bytes) B → B (1 byte) total written: 17 bytes buffer size: 7 bytes OVERFLOW: 10 bytes
확장 비율은 `7 + (n_escapable * 2)`이며, 여기서 `n_escapable`은 캡처 내 `+`, `%`, `&`의 개수입니다.
---
## 3. 공격 메커니즘
### 개요
| 단계 | 기법 | 설명 |
|------|-----------|-------------|
| 1 | 오버플로우 | `+` 패딩이 포함된 조작된 URI를 전송하여 힙 버퍼를 오버플로우 |
| 2 | 힙 스프레이 | `/spray`에 대용량 본문을 POST하여 제어된 데이터로 힙을 채움 |
| 3 | Feng Shui | 오버플로우 대상(`ngx_pool_cleanup_t`)이 인접하도록 할당 배치 |
| 4 | 핸들러 변조 | 오버플로우가 `ngx_pool_cleanup_t.handler`를 `system()` 주소로 덮어씀 |
| 5 | 정리 트리거 | 풀 소멸을 기다렸다가 → `system(cmd)`가 공격자 명령을 실행 |
| 6 | 리버스 셸 | 대화형 접근을 위해 리버스 셸 페이로드로 연결 |
### 크로스 요청 Feng Shui
**단일 요청 Feng Shui는 실패**합니다. 오버플로우가 `cleanup` 포인터에 도달하기 전에 풀의 메타데이터(`->d.next`, `->d.failed`)를 손상시키기 때문입니다. 요청 종료 시 풀이 소멸되면, 손상된 메타데이터로 인해 **`system()`이 호출되기 전에 충돌**이 발생합니다.
대신, 익스플로잇은 **크로스 요청 Feng Shui**를 사용합니다:
1. **요청 1 (스프레이)**: `/spray`에 대용량 본문을 POST합니다. 백엔드(`server.py`)가 `X-Delay` 헤더로 응답을 보류하여 연결을 유지하고 힙 할당을 보존합니다. 스프레이는 힙을 가짜 `ngx_pool_cleanup_t` 블록으로 채웁니다.
2. **요청 2 (오버플로우)**: 오버플로우 URI를 전송합니다. 오버플로우는 풀 메타데이터가 아닌 `cleanup` 포인터만 손상시켜, 스프레이된 가짜 블록을 가리키도록 합니다.
3. **풀 소멸**: 스프레이 응답이 완료되면(지연 만료), 풀의 정리 체인이 가짜 블록으로 이동하여 `system(cmd)`를 호출합니다.
### 주소 요구 사항
| 심볼 | 값 (Docker, ASLR 해제) | 설명 |
|--------|--------------------------|-------------|
| `HEAP_BASE` | `0x555555659000` | nginx 힙의 기준 주소 |
| `system@libc` | `0x7ffff6f6e420` | glibc의 `system()` |
| `NGX_CYCLES_POOL` | `0x5555556a4040` | 사이클 풀 포인터 |
| 가짜 정리 주소 | `0x5555556a4030` | 스프레이 대상 주소 |
### ASLR 우회
ASLR을 비활성화하지 않더라도 **DoS**(충돌)는 결정적으로 작동합니다. ASLR이 활성화된 상태에서 RCE를 달성하려면 두 가지 접근 방식이 있습니다:
1. **부분 덮어쓰기**: 1바이트 또는 2바이트 덮어쓰기를 사용하여 동일한 페이지 내에서 포인터를 이동시키고, 남은 니블을 브루트포싱합니다(16~256회 시도).
2. **정보 누출**: `/proc/self/maps`를 읽거나 `log_parser.py` 메모리 분석을 사용하여 레이아웃을 확인합니다.
---
## 4. 수정 분석
### 공식 수정
**커밋**: `524977e7c534e87e5b55739fa74601c9f1102686`
**파일**: `src/http/ngx_http_script.c`
**라인**: ~1205 (`ngx_http_script_regex_end_code` 내)```diff
void
ngx_http_script_regex_end_code(ngx_http_script_engine_t *e)
{
ngx_http_script_regex_code_t *code;
code = (ngx_http_script_regex_code_t *) e->ip;
+ e->is_args = 0; /* ← THE FIX */
e->ip += sizeof(ngx_http_script_regex_code_t);
// ...
}
ngx_http_script_regex_end_code는 모든 정규식 평가 후 길이 및 복사 패스 동안 실행됩니다. 여기서 e->is_args = 0을 재설정하면 다음이 보장됩니다:
set, if, rewrite)는 is_args = 0 상태로 시작합니다.ngx_http_script_start_args_code는 교체 문자열에서 ?를 만나면 여전히 is_args = 1을 설정할 수 있습니다. 이 수정은 해당 기능을 손상시키지 않습니다.patches/0002-hardening-bounds-check.patch는 ngx_http_script_copy_capture_code에 범위 검사를 추가합니다.```c
if (e->pos + len > e->buf.data + e->buf.len) {
return; /* gracefully truncate instead of overflowing */
}
### 백포트 패치
| 패치 | Nginx 버전 |
|-------|---------------|
| `patches/0001-fix-is_args.patch` | 1.22.x, 1.24.x, 1.26.x, 1.30.0 |
| `patches/backport-1.22.x.patch` | 1.22.0–1.22.1 |
| `patches/backport-1.24.x.patch` | 1.24.0–1.24.1 |
| `patches/backport-1.26.x.patch` | 1.26.0–1.26.1 |
---
## 5. 영향을 받는 버전
### NGINX 오픈 소스