
Cisco RV110w UPnP 스택 오버플로우
최근 UPnP가 뜨겁습니다. 마침 손에 Cisco RV110W가 하나 있습니다. 2021년 8월 시스코 공식에서 Cisco RV 시리즈의 UPnP 0day를 공개했지만 구체적인 세부 사항은 공개되지 않았습니다. 그래서 가지고 있는 장비로 이 취약점을 디버깅하고 분석해보려고 합니다. 취약점 공지는 공식 사이트에서 확인할 수 있습니다.
먼저 펌웨어를 최신 버전 1.2.2.8로 업데이트합니다. 다음으로 마주한 문제는 어떻게 디버깅하고 취약점을 찾을 것인가입니다. 먼저 디버깅 문제를 해결합니다. 디버깅의 첫 번째 작업은 장치의 셸을 얻는 것입니다. 그러나 최신 펌웨어는 디버깅 인터페이스를 제공하지 않습니다. 필자는 UART 직렬 포트와 펌웨어 패키지 수정을 통해 최신 펌웨어의 디버깅 권한을 얻었습니다. 구체적인 방법은 이전에 작성한 글 라우터 디버깅의 getshell을 참고하세요.
Cisco RV110W는 mipsel 아키텍처이므로 해당 gdb-server가 필요합니다. 직접 크로스 컴파일하거나 이미 컴파일된 것을 사용할 수 있습니다. 여기서는 gdb-static-cross를 추천합니다.
공식 공지에서 취약점이 UPnP 서비스에 존재한다고 밝혔습니다. 먼저 백엔드 관리자에 접속하여 FireWall의 Basic Settings에서 UPnP 구성을 켭니다.

그런 다음 nmap으로 포트를 스캔했지만 UPnP 포트가 발견되지 않았습니다. 그러나 테스트 결과 UPnP가 실제로 열려 있음을 확인했습니다.

필자는 UPnPy를 사용하여 취약점을 테스트하고 악용했습니다.
import socket
msg = \
b'M-SEARCH * HTTP/1.1\r\n' \
b'HOST:239.255.255.250:1900\r\n' \
b'ST:upnp:rootdevice\r\n' \
b'MX:2\r\n' \
b'MAN:"ssdp:discover"\r\n' \
b'\r\n'
# Set up UDP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.bind((b"192.168.2.100",23333)) #本机IP
s.settimeout(2)
s.sendto(msg, (b'239.255.255.250', 1900))
addr = ('192.168.2.1', 1900) # 网关IP
try:
while True:
data, addr = s.recvfrom(65507)
print(addr,data)
except socket.timeout:
pass
UPnP 응답을 실제로 수신했으며, 장치의 프로세스에 해당 UPnP 프로세스가 존재함을 확인했습니다.

UPnP는 일반적인 프로토콜 표준으로, 대부분의 제조사가 표준에 따라 구현합니다. 즉, 많은 액션이 동일하지만, 장치가 제공하는 서비스를 분석하여 취약점 위치를 찾고 악용하는 것도 필요합니다. 마찬가지로 UPnPy를 사용하여 정보를 수집합니다.
import upnpy
import socket
import requests
from upnpy.ssdp.SSDPDevice import SSDPDevice
msg = \
b'M-SEARCH * HTTP/1.1\r\n' \
b'HOST:239.255.255.250:1900\r\n' \
b'ST:upnp:rootdevice\r\n' \
b'MX:2\r\n' \
b'MAN:"ssdp:discover"\r\n' \
b'\r\n'
# Set up UDP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.bind((b"192.168.2.100",23333))
s.settimeout(2)
s.sendto(msg, (b'239.255.255.250', 1900))
addr = ('192.168.2.1', 1900)
data = b""
try:
while True:
data, addr = s.recvfrom(65507)
print(addr,data)
except socket.timeout:
pass
# data = b'HTTP/1.1 200 OK\r\nCache-Control: max-age=120\r\nDate: Fri, 01 Jan 2010 00:44:16 GMT\r\nExt: \r\nLocation: http://192.168.2.1:1780/InternetGatewayDevice.xml\r\nServer: POSIX UPnP/1.0 linux/5.70.48.16\r\nST: upnp:rootdevice\r\nUSN: uuid:31474a87-67ea-dae4-2f73-f157fb06d22b::upnp:rootdevice\r\n\r\n'
# data = b'HTTP/1.1 200 OK\r\nCache-Control: max-age=3600\r\nST: upnp:rootdevice\r\nUSN: uuid:824ff22b-8c7d-41c5-a131-8c3bad401726::upnp:rootdevice\r\nEXT:\r\nServer: Unspecified, UPnP/1.0, Unspecified\r\nLocation: http://192.168.3.1:56688/rootDesc.xml\r\n\r\n'
device = SSDPDevice(addr, data.decode())
services = device.get_services()
services_id = [services[i].id.split(":")[-1] for i in range(len(services))]
for id in services_id:
service = device[id]
actions = service.get_actions()
for action in actions:
for argument in action.arguments:
print(id,action.name,argument.name)
일련의 서비스 정보를 얻을 수 있으며, 일부 정보는 다음과 같습니다.
WANIPConn1 AddPortMapping NewRemoteHost
WANIPConn1 AddPortMapping NewExternalPort
WANIPConn1 AddPortMapping NewProtocol
WANIPConn1 AddPortMapping NewInternalPort
WANIPConn1 AddPortMapping NewInternalClient
WANIPConn1 AddPortMapping NewEnabled
WANIPConn1 AddPortMapping NewPortMappingDescription
WANIPConn1 AddPortMapping NewLeaseDuration
WANIPConn1 DeletePortMapping NewRemoteHost
WANIPConn1 DeletePortMapping NewExternalPort
WANIPConn1 DeletePortMapping NewProtocol
WANIPConn1 GetExternalIPAddress NewExternalIPAddress
이러한 서비스는 대체로 get 클래스, set 클래스 및 소수의 delete 클래스 서비스로 나눌 수 있습니다. UPnP의 주요 목적 중 하나는 내부 네트워크 장치를 외부 네트워크 장치에 노출시키는 것이므로, 구성(포트 매핑 등)이 필요합니다. 구성이 필요하므로 구성 매개변수와 정보는 필수적이며, 이러한 매개변수는 실제로 set 클래스 서비스의 매개변수입니다.
모든 서비스가 제조사에서 구현된 것은 아니므로 직접 펌웨어를 리버스 엔지니어링해야 합니다. 먼저 upnp_mainloop을 찾습니다.

모든 처리 로직은 upnp_dispatch에서 구현됩니다. 추적 분석한 결과 ssdp 및 http 요청 처리 부분이 발견되었습니다.

ssdp_process는 주소 지정 시 M-Search 요청에 해당하고, upnp_http_process는 http 요청 처리에 해당합니다. 정상적인 서비스 호출은 모두 http 요청이므로, upnp_http_process에 취약점이 있을 가능성이 있다고 판단합니다. 서비스 호출 예시는 아래 그림과 같습니다.

upnp_http_process에서 추가로 upnp_http_fsm_engine을 호출합니다.

추적 분석 결과, off_45ab80에 있는 여러 함수가 실행됩니다.

이 함수들은 초기화 및 프로토콜 헤더 구문 분석 기능을 포함합니다. 마지막 함수는 upnp_http_fsm_dispatch로, 해당 서비스 함수를 실행할 것으로 추측됩니다.

upnp_http_fsm_dispatch를 추적한 결과, 실제로 함수 메서드를 호출하지만 a1 및 a2에 따라 함수 호출이 실행되므로 구체적인 호출 함수를 확인할 수 없습니다. 동적 디버깅이 필요합니다.

정상적인 ssdp 주소 지정 요청인 경우 SUB_405B34, description_process를 호출하고, 서비스 관련 요청인 경우 soap_process를 호출합니다. soap_process에서는 요청 헤더 정보에 따라 query_process 또는 action_process를 호출합니다.
주로 action_process에 주목합니다. 서비스 호출 요청 헤더를 기반으로 action_process의 soap_control이 서비스 호출에 해당할 것으로 추측됩니다.

soap_control에서도 동적 디버깅이 필요하며, 구체적인 함수 정보를 확인합니다.

최종적으로 동적 디버깅을 통해 sub_414C28이 AddPortMapping 액션에 해당함을 확인했습니다. 해당 함수 호출 체인은 다음과 같습니다.
sub_414c28->upnp_portmap_add->upnp_osl_nat_config->strcpy(stack overflow)
그러나 upnp_portmap_add에서 장치의 WAN 포트 주소를 확인합니다. 필자는 테스트 시 WAN 포트를 구성하지 않았기 때문에 nvram을 사용하여 WAN 포트 IP를 설정했습니다.

스택 오버플로 시, 프로그램 흐름을 빨간색 사각형으로 제어해야 합니다. 파란색 사각형의 함수가 오버플로된 스택에 접근하여 RCE 전에 프로그램이 충돌하기 때문입니다. 따라서 *(a2+11)의 값이 0이 되도록 제어해야 합니다. 다행히 이 값은 제어 가능합니다.

최신 펌웨어에는 telnetd가 없으므로, 리버스 셸을 띄운 후 직접 utelnetd를 설치하고 telnet을 활성화할 수 있습니다.

리버스 셸 방법을 참고하세요.