
CVE-2020-12124에 대한 개념 증명(Proof-of-Concept) 구현
원본: https://www.klogixsecurity.com/scorpion-labs-blog/anatomy-of-an-iot-exploit-from-hands-on-to-rce
David E. Baker 작성, 2023년 6월 1일 게시
본 연구는 2020년 6월 기준 Wavlink Wireless-AC1200 기가비트 라우터의 펌웨어를 대상으로 합니다. 여기서 논의된 취약점은 공급업체에서 패치되었을 수도 있고 그렇지 않을 수도 있지만, 이는 여정 자체가 결론보다 더 큰 가치를 지니는 취약점 연구 사례입니다. 저자는 이 취약점들이 공개적으로 알려지기 전에 연구를 수행했지만, 다른 연구자들에 의해 독립적으로 발견되어 공급업체에 보고된 후의 시점이었습니다.
공급업체는 제품 펌웨어를 웹사이트 지원 섹션에서 제공합니다. 이는 IoT 펌웨어를 확보하는 일반적인 방법이며, 장치 메모리에서 추출하는 것보다 유용한 대안입니다. 펌웨어는 암호화되지 않았으므로 binwalk로 쉽게 추출할 수 있습니다. 동적 분석은 실제 장치 샘플에 접근하여 수행되었고, 정적 분석은 Ghidra를 통해 이루어졌습니다.
Wavlink Wireless-AC1200 기가비트 라우터의 웹 인터페이스에는 여러 취약한 엔드포인트가 존재하여, 사용자가 제공한 데이터를 제한 없이 애플리케이션 스택이나 명령줄에 직접 복사함으로써 임의 명령 실행이 가능합니다.
초기 장치 스캔 결과, 유일하게 노출된 리소스는 LAN 인터페이스에서 TCP 포트 80을 통해 HTTP로 접근 가능한 인증된 사용자용 관리 웹 콘솔뿐이었습니다. 장치가 더 많은 서비스를 제공할 수 있지만, 기본적으로 활성화되어 있지는 않습니다. 따라서 이 조사는 웹 인터페이스에만 집중했습니다.
샘플 장치에 대한 nmap 스캔 결과, 웹 인터페이스만 수신 대기 중인 모습.
일반적인 테스트(예: ping 또는 traceroute 명령의 매개변수에 명령 주입이 가능한 장치 진단 패널에서의 전형적인 명령 주입)는 즉시 흥미로운 결과를 내지 못해 실망스러웠습니다.
관리 웹 패널에 인증 후 사용 가능한 관리 옵션. 두 번째 옵션으로 "USB 저장소"가 보입니다.
여기서 최종적으로 익스플로잇이 가능했던 첫 번째 인터페이스는 위 스크린샷에서 두 번째 옵션으로 보이는 "USB 저장소" 패널에서 발견되었습니다. 이 장치는 802.2 이더넷 플러그 옆에 USB 포트를 갖추고 있어 네트워크 연결 스토리지(NAS) 기능을 제공할 수 있음을 시사합니다.
실제 샘플 뒷면 사진, USB 사용 가능 여부 표시.
취약점 연구의 간단한 원칙은 코드가 상호작용하는 구성 요소가 많고 움직이는 부분이 많을수록 근처에 익스플로잇 가능한 코드가 있을 가능성이 높다는 것입니다. NAS 기능의 존재는 장치의 소프트웨어 계층, 하드웨어 계층, 그리고 연결된 주변 장치(USB 저장소 자체)와 동시에 상호작용하는 코드가 있음을 나타내기 때문에 유망합니다.
USB 저장소 콘솔의 관리 인터페이스는 아래와 같습니다. "Workgroup" 필드만으로도 유망한데, 이 WiFi 라우터가 Server Message Block(SMB)을 통해 상호작용을 시도할 수 있음을 시사하기 때문입니다. 이는 IoT 라우터로서는 큰 도전입니다. 사용자 입력이 Unix smbpasswd 함수의 인수로 명령줄에 직접 전달되는 사례를 수없이 봐왔습니다.
인증된 사용자가 사용할 수 있는 USB 저장소 옵션.
이러한 설정을 조작하려는 초기 시도는 장치가 USB 드라이브를 감지하지 못해 실패했습니다. 아래와 같습니다.
적절한 형식의 드라이브가 장치의 USB 포트에 수동으로 연결되지 않으면 USB 저장소 옵션에 대한 구성 변경 사항이 저장되지 않습니다.
그러나 올바른 형식의 드라이브가 연결되면 장치에서 FTP 사용자 이름과 비밀번호를 설정할 수 있었습니다. 예상대로 사용자 입력을 명령줄에 배치했습니다:
'password' 필드의 명령 주입으로 장치 운영 체제에 첫 번째 셸 접근이 가능해집니다.
흥미롭긴 하지만, 이 취약점은 크게 기뻐하기 어렵습니다. 장치의 관리 인터페이스에 대한 인증된 접근뿐만 아니라 USB 드라이브를 조작하기 위한 물리적 접근도 필요하기 때문입니다. 위 익스플로잇을 통해 연구자는 개별 운영 체제 구성 요소와 상호작용하고 이를 역공학 목적으로 추출할 수 있습니다.
해당 장치는 Lighttpd로 구동되는 웹 인터페이스를 가진 BusyBox 중심의 Linux 시스템이었습니다. CGI(Common Gateway Interface) 기능은 /etc_ro/lighttpd/www/cgi-bin/ 디렉토리의 개별 바이너리로 제공되었으며, CGI URI에 대한 웹 요청이 이러한 바이너리를 직접 실행했습니다. Ghidra에서 nas.cgi를 빠르게 살펴보면 아래 38번째 줄에서 명령 주입을 확인할 수 있습니다. 사용자가 제공한 비밀번호가 do_system 함수(표준 libc system 호출을 감싸는 래퍼)로 직접 전달됩니다.
사용자 입력이 38번째 줄에서 chpasswd.sh 스크립트의 인수로 명령줄에 배치되어 명령 주입이 발생하고 장치 운영 체제에 직접 셸 접근이 가능해집니다.
/cgi-bin/ 디렉토리를 살펴보면 더 흥미로운 익스플로잇을 찾는 작업은 사용자에게 제공되는 CGI 인터페이스를 열거하는 것으로 축소됩니다. 아래와 같습니다:
장치에서 제공되는 CGI 바이너리의 전체 목록으로, 이 섹션에서 설명된 익스플로잇을 통해 확보한 셸에서 직접 가져온 것입니다. 사용자 입력이 38번째 줄에서 chpasswd.sh 스크립트의 인수로 명령줄에 배치되어 명령 주입이 발생하고 장치 운영 체제에 직접 셸 접근이 가능해집니다.
초기 검토에서 몇 가지가 눈에 띕니다. 첫 번째로 중요한 점은 CGI 바이너리가 종종 check_valid_user 함수를 호출한다는 것입니다. 이 메서드는 요청을 보낸 IP 주소가 파일 시스템의 특정 임시 파일에 저장되어 있는지 확인합니다. 최소한의 테스트에 따르면 클라이언트의 인증 상태는 이 메서드가 호출될 때까지 확인되지 않으므로, 각 CGI 바이너리에서 이 함수 호출 이전의 모든 코드 영역은 인증 없이 접근 가능합니다.
adm.cgi의 디컴파일 결과, check_valid_user 메서드가 표시됩니다. 이 호출 이전의 모든 코드는 요청자의 인증 상태를 확인하기 전에 실행됩니다.
또 다른 흥미로운 관찰은 사용자 제공 입력을 스택에 직접 복사하는 많은 메서드입니다. 예를 들어, wireless.cgi의 디컴파일 결과 웹 요청 본문에서 가져온 NewName 매개변수 인수가 14, 15번째 줄에 나타나고, 아래 34번째 줄에서 이 사용자 입력이 보호되지 않은 strcpy를 통해 스택에 복사됩니다:
14번째 줄과 34번째 줄은 요청 본문의 NewName 매개변수가 프로그램 스택에 직접 보호되지 않은 strcpy로 복사되는 것을 보여줍니다. adm.cgi의 디컴파일 결과, check_valid_user 메서드가 표시됩니다. 이 호출 이전의 모든 코드는 클라이언트의 인증 상태를 확인하기 전에 실행됩니다.
이러한 사용자 입력의 스택 복사만으로도 메모리 손상 익스플로잇이 가능함을 시사하며, 다음 명령으로 확인할 수 있습니다:
curl -XPOST --data "page=SetName&NewName=\`python3 -c 'print(\\"A\\"*(512))'\`" http://target-ip/cgi-bin/wireless.cgi
유망하지만, 여기서 Return-Oriented 공격은 최적의 선택이 아닙니다. 이미 확보한 셸 접근을 통해 다음 명령을 실행하면 '1'이 표시되며, 이는 ASLR(Address Space Layout Randomization)이 약하게 적용되어 ROP 체인이 원하는 가젯에 도달할 확률이 1/256임을 나타냅니다:
# cat /proc/sys/kernel/randomize_va_space
1
또한 아래 명령은 little-endian 바이너리가 스택 가드 바이트로 컴파일되었음을 보여주므로, 체인의 마지막 가젯만이 바이너리 내 미리 결정된 위치에 도달할 수 있습니다. 충돌 후 웹 서버를 재시작하는 감시 프로세스가 있다면 반복적으로 Return-Oriented 익스플로잇을 던져 결국 성공할 가능성도 배제할 수 없지만, 코드의 다른 곳에 더 나은 버그가 숨어 있을 가능성도 있습니다.
xxd /etc_ro/lighttpd/www/cgi-bin/wireless.cgi | head -n 10
CGI 함수들을 계속 살펴보면 결국 live_api.cgi를 검토하게 됩니다. 이 바이너리는 check_valid_user를 전혀 호출하지 않으므로, URI /cgi-bin/live_api.cgi에 대한 모든 웹 요청은 인증 없이 이 CGI 애플리케이션을 실행합니다. 그림 11의 9번째 줄에서 QUERY_STRING 환경 변수(Apache CGI 규격에 따르면 요청 URI에서 물음표 바로 뒤에 오는 부분이므로 사용자 제공)가 pcVar1에 저장되고, 그림 11의 19번째 줄에서 satellite_status 메서드로 전달됩니다.
live_api.cgi의 디컴파일 결과, 9번째 줄에서 URI로부터 사용자 입력을 받아 19번째 줄에서 satellite_status로 전달됩니다. adm.cgi의 디컴파일 결과, check_valid_user 메서드가 표시됩니다. 이 호출 이전의 모든 코드는 클라이언트의 인증 상태를 확인하기 전에 실행됩니다.
그림 12에 표시된 satellite_status 메서드의 디컴파일 결과, 쿼리 문자열(현재 param_1)이 page, id, ip 매개변수로 파싱됩니다. ip 매개변수는 그림 12의 38번째 줄에서 sprintf 함수를 통해 로컬 변수에 복사되고, 39번째 줄에서 do_system 함수로 전달됩니다. check_user_auth 호출이 없다는 것은 URI 내 인증되지 않은 사용자의 임의 입력이 ip URI 매개변수를 통해 명령줄에 직접 배치된다는 것을 의미하며, 이는 다음과 같이 확인됩니다:
satellite_status 함수의 디컴파일 결과, 쿼리 문자열(이제 param_1)이 22, 23번째 줄에서 ip 매개변수로 파싱된 후 38, 39번째 줄에서 do_system 호출로 전달됩니다.
결과가 증명하듯, 익스플로잇을 사용하여 장치를 원격으로 장악하는 모습입니다.
시장에 새로 등장한 IoT 장치에서 익스플로잇을 찾는 것은 이 글의 서두에서 언급했듯이 쉬운 표적으로 보일 수 있지만, 이 조사의 가치는 결과보다는 그 여정에 있었습니다.
인증 우회와 명령 주입의 위치를 이해하는 것은 펌웨어의 정적 분석 없이는 불가능했을 것입니다. 펌웨어가 온라인에서 제공되지 않았다면, 장치 운영 체제에 대한 동적 접근이 필요했을 것이며, 그러한 접근은 희박했을 것입니다. 이러한 접근 수준 자체도 장치에 대한 물리적 접근과 특수 하드웨어, 또는 오류가 발생할 가능성이 높은 코드 위치에 대한 유도된 추측에 의존했습니다.
이 여정을 즐기셨기를 바라며, 앞으로도 다시 찾아주시길 기대합니다. 해킹을 즐기세요!
저자:
David Baker, Senior Security Consultant, Testing, K logix