
SolarWinds Serv-U CVE-2026-28318: 인증되지 않은 Content-Encoding: deflate 크래시. 근본 원인 분석 (내부 포인터의 잘못된 해제 -> 힙 손상) + DoS 전용 PoC. 15.5.4 핫픽스 1에서 수정됨.
근본 원인 분석 + DoS 개념 증명. 공식 권고에서는 이를 인증되지 않은 서비스 거부 / 제어되지 않은 리소스 소비로 분류합니다. 바이너리 분석 결과 근본적인 결함은 메모리 안전성입니다: HTTP
deflate디코드 경로에서 내부 포인터에 대한 유효하지 않은free()가 발생하여 프로세스 힙을 손상시킵니다(STATUS_HEAP_CORRUPTION,0xC0000374). 이는 압축 해제 폭탄이 아니며, 충돌은 압축 해제된 크기와 무관합니다.
이는 HTTP/HTTPS 리스너(웹/관리 경로)에만 영향을 미칩니다. FTP/FTPS/SFTP는 이 코드 경로에 있지 않습니다.
Content-Encoding: deflate 헤더와 유효한 deflate 본문을 포함한 모든 HTTP 요청은 서비스를 충돌시킵니다. 핸들러는 본문을 압축 해제한 다음 압축된 본문에 대한 포인터를 해제하고 압축 해제된 버퍼로 교체합니다. 그러나 그 포인터는 HTTP 수신 버퍼 내의 내부 포인터입니다 (본문을 가리키며, \r\n\r\n 바로 뒤에 위치). 힙 할당 기준이 아닙니다. 기본이 아니고 정렬되지 않은 포인터에 대해 free()를 호출하면 힙이 손상되고 프로세스가 종료됩니다.
버그가 본문 포인터가 해제되는 방식에 있기 때문에 — 얼마나 많은 데이터가 생성되는지가 아니라 — 약 25바이트의 압축된 본문(수 KB로 압축 해제됨)도 안정적으로 충돌을 일으킵니다. 메모리 사용량은 증가하지 않으며, 'zip/deflate 폭탄'이 아닙니다.
deflate 디코드 루틴은 RhinoNET.dll(Serv-U의 네트워크 라이브러리)에 있으며, 헤더 파서가 Content-Encoding: deflate를 보고 'deflate' 플래그를 설정하면 HTTP 수신 경로(ProcessReceive)에서 도달합니다.
단순화하면, 루틴은 decode(this, &bodyPtr, &bodyLen)으로 호출되며 다음을 수행합니다:
1. inflate bodyPtr[0..bodyLen] -> grows an accumulator buffer `acc` (stock zlib, bounded, correct)
2. free(*bodyPtr) <-- *bodyPtr is an INTERIOR pointer into the receive buffer
3. *bodyPtr = acc // replace compressed body with decompressed buffer
4. *bodyLen = total
2단계가 버그입니다. *bodyPtr는 독립적으로 할당된 블록이 아닙니다: 요청 본문을 가리키지만 단일 HTTP 수신 버퍼 내부에 있습니다, 즉 receive_buffer + header_length입니다. 내부(그리고 16바이트 정렬되지 않은) 포인터를 해제하면 할당자가 공격자가 영향을 미친 영역을 힙 청크 헤더로 파싱하게 되어 → 힙 메타데이터 손상 → 0xC0000374가 발생합니다.
두 가지 '명백한' 가설은 틀렸으며, 런타임에서 확인되었습니다:
zlib1.dll!inflate이며 모든 호출에서 avail_out을 준수합니다 (100개 이상의 청크에서 관찰됨; 범위를 벗어난 쓰기 없음).prev_total + produced + 1로 할당되고 정확히 그만큼 기록됩니다 — 빡빡하며 오버플로우가 없습니다.손상은 오로지 2단계의 유효하지 않은 free() 때문입니다.
런타임(Frida)에서 Serv-U 15.5.4.108을 대상으로 본문이 "A" * 8192로 압축 해제되는 Content-Encoding: deflate 요청을 전송하는 동안 캡처:
…ce — mod 16 == 14입니다. 힙 할당 기준은 16바이트 정렬이므로 할당 기준이 아닙니다; 내부 포인터입니다.\r\n\r\n으로 끝납니다:
…tream\r\nContent-Length: 26\r\nConnection: close\r\n\r\n
ed c1 01 0d 00 …).ntdll.dll, 예외 0xC0000374 (STATUS_HEAP_CORRUPTION), 호출은 RhinoNET.dll deflate 핸들러의 free에서 시작됩니다. 충돌 전 마지막 힙 작업이 바로 이 free(bodyPtr)입니다.0xC0000374)하며, 코드 실행을 달성하지 못했습니다. 이를 메모리 안전성 근본 원인을 가진 사전 인증 힙 손상 DoS로 취급하십시오; RCE로 가정하지 마십시오.Python 3(표준 라이브러리만 필요)이 필요합니다. 테스트 권한이 있는 Serv-U HTTP 리스너를 대상으로 설정하십시오. Windows에서는 하나의 Get-NetTCPConnection 호출을 사용하여 리스너 PID를 읽고 응용 프로그램 이벤트 로그를 확인하여 힙 손상 충돌을 확인합니다.
python poc_verify.py # default 127.0.0.1:80, tiny packet, 1 shot
python poc_verify.py --host <ip> --port 80
python poc_verify.py --big # body decompresses to 8192 bytes
python poc_verify.py --shots 3
python poc_verify.py --no-events # skip event-log check (no privileges needed)
PoC는 최소한의 요청을 구성합니다:
POST / HTTP/1.1
Host: <target>
Content-Encoding: deflate
Content-Type: application/octet-stream
Content-Length: <n>
Connection: close
<raw-deflate body — even a few dozen bytes is enough>
서비스가 충돌한 경우(리스너 PID 변경 / 0xC0000374 이벤트 발생) PASS를 보고하고, 서비스가 생존한 경우(이미 패치됨, 영향을 받지 않음, 또는 본문이 deflate로 처리되지 않음) FAIL을 보고합니다.
Content-Encoding을 제거하거나 거부합니다, 예: 리버스 프록시에서:
if ($http_content_encoding) { return 400; }
RhinoNET.dll / Serv-U.exe(이미지 베이스 0x180000000)의 매개변수화된 PE 디스어셈블리를 통해 수신 → 헤더 파싱 → deflate 디코드 경로를 매핑하고 오류가 있는 free를 찾았습니다.zlib가 avail_out을 준수함을 증명하기 위해 압축 해제기를 후킹하고, 디코드 호출의 모든 alloc/free/memcpy를 디스크에 추적하며, 프로세스 예외 핸들러를 통해 손상 시점의 오류 명령과 0xC0000374 스택을 캡처했습니다. 그런 다음 해제된 포인터 주변의 바이트(HTTP 헤더 꼬리 + 원시 deflate 본문, 16바이트 비정렬)를 덤프하여 내부 포인터 해제를 확인했습니다.분석에서 참조된 오프셋은 빌드 15.5.4.108에 특화되어 있으며 빌드에 따라 다를 수 있습니다.
벤더 패치 제공 후 방어적 연구로 게시됨. PoC는 DoS 전용이며 승인된 테스트를 위한 것입니다.
| 제품 | SolarWinds Serv-U (FTP / MFT / File Server) |
| 취약 버전 | 15.5.4 and earlier in that branch without Hotfix 1 (analysis done on build 15.5.4.108) |
| 수정 버전 | Serv-U 15.5.4 Hotfix 1 (released 2026-06-04) |
| 공격 경로 | 인증되지 않음, 네트워크 (HTTP/HTTPS 관리/웹 포트) |
| 공개 분류 | CWE-400 Uncontrolled Resource Consumption · DoS · CVSS 7.5 · CISA KEV |
| 실제 분류 | CWE-763 Release of Invalid Pointer / CWE-590 Free of Memory not on the Heap → heap corruption |