Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2021-4045 — TP-Link Tapo C200 카메라(CVE-2021-4045)용 명령 주입 취약점 익스플로잇으로, UART를 통한 루트 셸 액세스 및 리버스 엔지니어링된 uhttpd 바이너리 분석을 제공합니다. | Kitploit
도구/GitHubGitHub/kaleth4/cve-2021-4045
Embedded Systems SecurityIoT SecurityVulnerability AnalysisExploitationReverse EngineeringHardware HackingPenetration TestingCommand and ControlFirmware Analysis
GitHubkaleth4/cve-2021-4045

CVE-2021-4045

TP-Link Tapo C200 카메라(CVE-2021-4045)용 명령 주입 취약점 익스플로잇으로, UART를 통한 루트 셸 액세스 및 리버스 엔지니어링된 uhttpd 바이너리 분석을 제공합니다.

2개월 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
저장소 보기

🔍 CVE-2021-4045: TP-Link Tapo C200 명령 주입 취약점

image

CVE-2021-4045


📌 요약

CVE-2021-4045는 TP-Link Tapo C200 카메라에서 발견된 명령 주입 취약점으로, 공격자가 root 권한으로 장치를 완전히 제어할 수 있습니다. 이 취약점은 펌웨어 버전 1.1.16 Build 211209 Rel. 37726N 이전의 모든 버전에 영향을 미칩니다.

🔗 INCIBE 공식 공지: https://www.incibe.es/incibe-cert/alerta-temprana/vulnerabilidades/cve-2021-4045 (실제 링크로 대체)

🔧 권장 해결 방법: 펌웨어를 1.1.16 이상 버전으로 업데이트하세요.



🛠 초기 인식

장치 구성 및 특징

  • 고급 기능을 갖춘 저렴한 IP 카메라 (~30€):
    • SD 카드 녹화.
    • 수평 360°, 수직 90° 회전.
    • 모바일 앱에서 실시간 오디오 재생.

포트 스캔```bash

$ nmap -sV -p- 192.168.1.81

root@kitploit:~
**결과**:```
PORT     STATE SERVICE
443/tcp  open  https
554/tcp  open  rtsp
2020/tcp open  xinupageserver
8800/tcp open  sunwebadmin
image 보시다시피, 장치에는 몇 가지 흥미로운 열린 포트가 있습니다. 가장 먼저 시도한 것은 포트 443이었습니다. nmap이 https를 사용한다고 명확히 표시했지만, 초기 스캔 시 이를 간과하고 포트 443이 http를 사용하고 있다고 생각하며 꽤 많은 시간을 보냈습니다. 이 때문에 http://192.168.1.81:443만 시도하고 https://192.168.1.81:443을 시도하지 않아 400 응답만 받았습니다. 서론에서 말했듯이, 이 과정은 실패로 가득 차 있었습니다. 다른 포트들의 경우, 그 포트에서 실행 중인 서비스들은 저에게 완전히 낯선 것이었고 이에 대한 명확한 정보도 찾을 수 없었습니다. 그 시점에서 저는 더 이상 알려진 방법이 없었기 때문에 더 깊이 조사할 때였습니다.

----[ 쉘 획득 ]-------------------------------

카메라를 구매하기 전에 인터넷에서 해당 장치에 대한 이전 연구를 검색했고, 운 좋게도 사람들이 역공학을 위해 협력하고 있는 이 GitHub 저장소를 찾았습니다. 그 이슈 중 하나는 UART 포트를 통해 콘솔에 접근하는 방법을 설명하고 있었는데, 당시 저는 UART에 대해 전혀 몰랐습니다. 그래서 기본을 배우고 연결을 위해 USB to TTL 변환기를 구입했습니다. image 언급된 이슈의 도움으로 칼과 드라이버로 장치를 열고 UART를 빠르게 찾을 수 있었습니다. 몇 번의 시도와 많은 인내 끝에 마침내 패드에 몇 개의 전선을 납땜할 수 있었습니다.

image

그런 다음, 납땜이 데이터 전송에 충분히 좋은지 확인할 때였습니다. UART의 Rx는 어댑터의 Tx로, 그 반대도 마찬가지라는 점을 염두에 두고 전선을 USB 어댑터에 연결한 후 어댑터를 컴퓨터에 연결했습니다. 다시 언급된 이슈 덕분에 시리얼 연결의 전송 속도가 57600임을 알고 다음을 실행했습니다:

$ sudo screen /dev/tty.usbserial-0001 57600

여기서 '/dev/tty.usbserial-0001'은 어댑터가 연결되어 장치에 전원을 공급하는 USB 포트입니다. 즉시 데이터를 수신하기 시작했습니다. 좋았습니다.

하지만 아직 콘솔에 접근할 수 없었습니다. 수신된 것은 단지 장치의 부트 시퀀스였으며, 실제로는 U-Boot 부트로더였습니다. 대략 다음과 같았습니다:

U-Boot 2014.01-v1.2 (Jul 16 2021 - 18:41:10)

Board: IPCAM RTS3903 CPU: 500M :rx5281 prid=0xdc02 force spi nor mode DRAM: 64 MiB @ 1066 MHz Skipping flash_init Flash: 0 Bytes flash status is 0, 0, 0 SF: Detected XM25QH64A with page size 256 Bytes, erase size 64 KiB, total 8 MiB Using default environment

Autobooting in 1 seconds copying flash to 0x81500000 flash status is 0, 0, 0 SF: Detected XM25QH64A with page size 256 Bytes, erase size 64 KiB, total 8 MiB SF: 8388608 bytes @ 0x0 Read: OK

[...]

Enter를 누르면 사용자 이름과 비밀번호를 입력하라는 메시지가 나타납니다. 그 GitHub 이슈 덕분에 자격 증명을 알고 있으므로 'root' 사용자와 'slprealtek' 비밀번호로 성공적으로 로그인하고 마침내 콘솔에 접근할 수 있었습니다.

연결이 작동하는 것을 확인한 후, 케이스 조립 과정에서 두 번이나 끊어졌던 납땜을 보강해야 했습니다. 모든 전선을 고정하기 위해 핫멜트 실리콘을 바르고 장치를 닫은 후 모든 모터를 분리했습니다. 이제 테스트 장치가 준비되었습니다.

image

----[ 장치 탐색 ]--------------------------

이제 케이스가 준비되었으니 장치를 탐색해 봅시다:

root@SLP:~# uname -a Linux SLP 3.10.27 #1 PREEMPT Wed Nov 11 20:42:05 CST 2020 rlx GNU/Linux

root@SLP:~# cat /etc/openwrt_version 12.09-rc1

보시다시피, Linux 3.10.27을 실행하는 OpenWRT 머신입니다. 이제 활성 프로세스와 열린 포트를 확인해 보겠습니다:

root@SLP:~# ps PID USER VSZ STAT COMMAND 1 root 2328 S init 2 root 0 SW [kthreadd] 3 root 0 SW [ksoftirqd/0] 4 root 0 SW [kworker/0:0] 5 root 0 SW< [kworker/0:0H] 6 root 0 SW [kworker/u2:0] 7 root 0 SW [rcu_preempt] 8 root 0 SW [rcu_bh] 9 root 0 SW [rcu_sched] 10 root 0 SW< [khelper] 11 root 0 SW< [writeback] 12 root 0 SW< [bioset] 13 root 0 SW< [kblockd] 14 root 0 SW [khubd] 15 root 0 SW [kworker/0:1] 16 root 0 SW [kswapd0] 17 root 0 SW [fsnotify_mark] 18 root 0 SW< [crypto] 27 root 0 SW [kworker/u2:1] 46 root 0 SW< [deferwq] 47 root 0 SW< [kworker/0:1H] 247 root 2328 S -ash 262 root 0 SW [irq/27-gpio res] 273 root 0 SW< [cryptodev_queue] 282 root 860 S /sbin/hotplug2 --override --persistent --set-rules-f 304 root 888 S /sbin/ubusd 325 root 8152 S tp_manage 357 root 3416 S /usr/bin/ledd 361 root 3408 S /sbin/msglogd 367 root 3220 S /usr/sbin/netlinkd 370 root 5468 S < /usr/bin/system_state_audio 379 root 10180 S /usr/sbin/wlan-manager 491 root 1636 S /sbin/netifd 492 root 1520 S /usr/sbin/connModed 494 root 11488 S /usr/bin/dsd 496 root 1532 S /usr/sbin/connModed 502 root 7640 S /bin/cloud-service 520 root 4360 S /bin/cloud-brd -c /var/etc/cloud_brd_conf 653 root 15020 S /bin/cloud-client 830 root 2320 S /usr/sbin/telnetd -b 127.0.0.1 861 root 3852 S /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C 870 root 6048 S /usr/bin/relayd 872 root 5948 S /usr/bin/rtspd 879 root 4612 S /usr/bin/p2pd 884 root 11152 S /bin/dn_switch 889 root 4180 S /bin/storage_manager 920 root 40940 S /bin/cet 956 root 32336 S /bin/vda 960 root 3808 S /bin/wtd 970 root 11288 S /bin/nvid 1019 root 2332 S udhcpc -p /var/run/static-dhcpc.pid -s /lib/netifd/s 1037 root 0 SW [RTW_CMD_THREAD] 1059 root 1212 S wpa_supplicant -B -Dwext -iwlan0 -P/tmp/supplicant_p 1089 root 2332 S /usr/sbin/ntpd -n -p time.nist.gov -p 133.100.9.2 -p 1103 root 3840 S /usr/bin/motord 1447 root 2324 R ps

root@SLP:~# netstat -natpu Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:8800 0.0.0.0:* LISTEN 920/cet tcp 0 0 127.0.0.1:929 0.0.0.0:* LISTEN 875/p2pd tcp 0 0 0.0.0.0:20002 0.0.0.0:* LISTEN 325/tp_manage tcp 0 0 0.0.0.0:2020 0.0.0.0:* LISTEN 969/nvid tcp 0 0 0.0.0.0:554 0.0.0.0:* LISTEN 920/cet tcp 0 0 127.0.0.1:23 0.0.0.0:* LISTEN 832/telnetd tcp 0 0 127.0.0.1:921 0.0.0.0:* LISTEN 878/relayd tcp 0 0 127.0.0.1:922 0.0.0.0:* LISTEN 877/rtspd tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 863/uhttpd tcp 0 0 192.168.1.80:37380 52.19.66.90:443 ESTABLISHED 507/cloud-brd udp 0 0 0.0.0.0:20002 0.0.0.0:* 325/tp_manage udp 0 0 0.0.0.0:38000 0.0.0.0:* 1087/ntpd udp 0 0 0.0.0.0:3702 0.0.0.0:* 969/nvid

nmap 스캔에서 보이는 열린 포트 뒤에 있는 프로세스(예: uhttpd 또는 cet)를 볼 수 있습니다. 저는 특히 uhttpd 프로세스에 집중했습니다. 이 프로세스가 https 서버(당시에는 여전히 http라고 생각했음) 뒤에 있었고, 이미 http 프로토콜에 매우 익숙했기 때문입니다.

uhttpd는 OpenWRT가 이 배포판을 실행하는 임베디드 장치에서 사용하기 위해 만든 웹 서버입니다. 이 시점에서 소스 코드나 적어도 경로와 같은 더 많은 정보를 얻을 수 있는지 알고 싶었습니다. OpenWRT 위키를 방문하여 uhttpd와 OpenWRT 전반에 대해 배웠습니다. OpenWRT 머신에는 UCI(Unified Configuration Interface)라는 시스템이 있는데, 기본적으로 시스템 서비스를 쉽게 구성하는 데 사용됩니다. 이를 사용하여 uhttpd의 구성을 얻을 수 있습니다:

root@SLP:~# uci show | grep uhttpd ucitrack.@uhttpd[0]=uhttpd ucitrack.@uhttpd[0].init=uhttpd uhttpd.main=uhttpd uhttpd.main.listen_https=443 uhttpd.main.home=/ww uhttpd.main.rfc1918_filter=1 uhttpd.main.max_requests=8 uhttpd.main.cert=/tmp/uhttpd.crt uhttpd.main.key=/tmp/uhttpd.key uhttpd.main.cgi_prefix=/cgi-bin uhttpd.main.lua_prefix=/luci uhttpd.main.lua_handler=/usr/lib/lua/luci/sgi/uhttpd.lua uhttpd.main.script_timeout=180 uhttpd.main.network_timeout=180 uhttpd.main.tcp_keepalive=0 uhttpd.px5g=cert uhttpd.px5g.days=3600 uhttpd.px5g.bits=1024 uhttpd.px5g.country=CN uhttpd.px5g.state=China uhttpd.px5g.location=China uhttpd.px5g.commonname=TP-Link upnpc.uhttpd=entry upnpc.uhttpd.proto=TCP upnpc.uhttpd.ext_port=80 upnpc.uhttpd.desc=uhttpd

여기 몇 가지 흥미로운 매개변수가 있습니다. 첫째, 'uhttpd.main.home'은 서버의 루트 디렉터리를 가리키므로 웹 서버 파일을 찾을 수 있을 것입니다. 그런 다음 'uhttpd.main.lua_handler'는 서버 시작 시 Lua 런타임 환경을 초기화하는 데 사용되는 Lua 핸들러 스크립트를 가리킵니다. uhttpd가 Lua 스크립트를 지원하기 때문에 거기에 더 흥미로운 파일이 있을 수 있습니다. 그러나 '/www' 디렉터리는 비어 있고 '/usr/lib/lua/luci'에는 'sgi' 디렉터리도, 시스템에 'uhttpd.lua' 파일도 없습니다. 이 uhttpd 인스턴스가 어떻게 작동하는지에 대한 정보를 찾으려고 했지만, 아무것도 찾을 수 없었고, 아무 곳도 가리키지 않는 구성 매개변수만 있었습니다.

이 시점에서 해결책은 uhttpd 바이너리를 직접 분석하고 역공학을 적용하는 것임을 알았지만, 그 전에 테스트 환경을 만들어 웹 서버 내부에서 요청을 보낼 때 어떤 일이 일어나는지 알고 싶었습니다. 프로세스가 생성된 방식으로 인해 어디에도 출력이 없었기 때문입니다.

ps 명령의 출력에서 프로세스 861에 대한 명령을 실행해 보았습니다:

$ /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C

그러나 많은 오류가 발생했고 작동시킬 수 없었습니다. 다른 포트에서 동일한 uhttpd 프로세스를 생성할 수 없었기 때문에, 프로세스의 '/proc' 항목을 확인하여 누락된 출력을 찾으려고 시도했습니다. (PwnFunction의 이 비디오에서 설명한 대로) 그러나 큰 문제가 있었습니다:

root@SLP:~# sudo ls -l /proc/864/fd/ lrwx------ 1 root root 64 Nov 10 22:44 0 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 1 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 2 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 3 -> anon_inode:[eventpoll] lrwx------ 1 root root 64 Nov 10 22:44 4 -> socket:[1830]

'stdin', 'stdout', 'stderr'에 대한 모든 파일 디스크립터가 '/dev/null'로 리디렉션되었는데, 이는 기본적으로 찾을 수 없는 블랙홀(black hole)로 리디렉션되는 것이었습니다. 막혀서 무엇을 해야 할지 몰랐습니다. 이미 '/proc' 항목에 있었기 때문에 조사하기 시작했는데, '/proc' 항목이 프로세스에 대한 그렇게 많은 정보를 가지고 있다는 것을 기억하지 못했고 호기심이 생겼기 때문입니다. 이 우연한 호기심 덕분에 해당 프로세스의 모든 환경 변수를 포함하는 'environ' 항목을 발견했습니다. 그 환경 변수 중 하나는 다음과 같았습니다:

UHTTPD_ARGS=-h /www -T 180 -A 0 -n 8 -R -r C200 -C /tmp/uhttpd.crt -K /tmp/uhttpd.key -s 443

즉시 ps에 표시된 명령이 올바르지 않다는 것을 깨달았고, 이후 UART 인터페이스에 모든 문자를 표시할 충분한 대역폭이 없었기 때문임을 발견했습니다. 또 다른 실패였지만 중요한 교훈을 가르쳐 주었습니다: UART 포트의 출력을 절대 신뢰하지 말라는 것입니다.

이제 마침내 동일한 매개변수로 uhttpd의 다른 인스턴스를 생성하고 '/dev/null'로 파이프하지 않고 바이너리를 역공학하면서 테스트할 수 있었습니다.

----[ Ghidra로 uhttpd 역공학 ]--------

Ghidra를 사용한 것은 이번이 처음이었습니다. 몇 가지 동영상을 보고 몇 가지 기사를 읽었지만(멋지고 이해하기 쉬운 콘텐츠를 제공한 stacksmashing과 liveoverflow에게 감사합니다), 실제로 사용해 본 적은 없었기 때문에 배우기에 아주 좋은 기회였습니다.

uhttpd 바이너리를 열었고, 여러 번 시도한 끝에 언어가 MIPS32, little endian, mips16e임을 발견했습니다. 일부 함수 이름은 바이너리와 함께 기본으로 제공되었지만, 다른 함수는 그렇지 않았습니다. 또한 함수 이름을 바꾸는 데 시간을 할애했습니다. Ghidra는 종종 외부 함수와 혼동되어 다음과 같은 이상한 래퍼(wrapper)를 생성하는 것 같았기 때문입니다:

image `main()` 함수와 다른 중요한 함수를 분석하여 바이너리의 논리와 구조를 이해했습니다. 이미 식별된 몇 가지 흥미로운 함수를 찾았는데, 여기에는 `do_login()`과 `uh_slp_proto_request()`가 포함되었습니다. 후자에 대해서는 나중에 더 이야기하겠습니다.

이 첫 접촉 후에 버그를 찾기 시작했습니다. 오버플로(overflow) 취약점에 대한 완전한 초보자로서 가장 먼저 한 일은 system(), exec(), popen() 호출을 검색하여 쉽게 악용할 수 있는 명령 주입 취약점이 있는지 확인하는 것이었습니다. 그리고 정말 운이 좋았습니다.

'exec_and_read_json()' 함수는 'popen()'을 사용하여 명령을 실행합니다:

ejecutar_y_leer_json

'exec_and_read_json()' 함수는 두 개의 이름 없는 함수에 의해 사용되며, 저는 이 함수들을 'set_language()' 및 'wifi_connect()'라고 명명했습니다. 이 함수들은 각각 언어 설정 및 Wi-Fi 연결(당연히)을 담당합니다. 'wifi_connect()'는 작은따옴표(')를 구문 분석하는 것으로 보이는 반면, 'set_language()'는 그렇지 않습니다. 이는 'set_language()' 함수의 입력을 제어할 수 있다면 명령을 성공적으로 주입할 수 있음을 의미합니다:

conexión wifi establecer_idioma

'set_language()' 함수는 앞서 언급한 'uh_slp_proto_request()' 함수에 의해 사용되며, 이 함수는 사용자로부터 받은 구문 분석된 일부 데이터를 입력으로 전달합니다.

función_principal_1 función_principal_2

사용자 데이터를 구문 분석하기 위해 uh_slp_proto_request()는 유효한 JSON 객체인지 확인합니다. 그런 다음 method 키로 식별되는 문자열 값과 params로 식별되는 딕셔너리 값을 가져옵니다(적어도 그렇게 생각합니다. Ghidra가 함수 호출을 해결할 수 없었지만 이렇게 작동하는 것 같았습니다). 선택된 메서드에 따라 uh_slp_proto_request()는 실행할 함수를 선택합니다.

따라서 다음 페이로드를 전송하면:{"method": "setLanguage", "params":{}}

'set_language()' 함수를 올바르게 호출하고 '{}'를 'language_json' 매개변수로 전달합니다. 그런 다음 'set_language()' 내부에서 'language_json' 객체가 문자열로 변환되어 "ubus call system_state_audio set_language \'%s\'"에 직접 삽입되어 실행됩니다.

이 페이로드를 전송할 때:{"method": "setLanguage", "params": {"payload": "'; touch poc;'"}}

다음이 실행됩니다.

ubus call system_state_audio set_language '{"payload": "'; touch poc;'"}'

실제로는 3개의 명령어를 포함합니다:

ubus call system_state_audio set_language '{"payload": "' touch poc '"}'

두 번째 명령어는 완전한 코드 실행을 가능하게 합니다.

이제 'uh_slp_proto_request()' 함수는 모든 요청을 관리하는 다른 이름 없는 함수(제가 'main_server_function()'이라고 명명함)에 의해 사용됩니다. 요청이 유효한 경우(최대 길이를 초과하지 않고, 서버 구성에 따라 'http' 또는 'https'를 사용하는 등), 'main_server_function()'은 URL에 '/cgi-bin/luci' 또는 '/web-static'이 포함되어 있는지 확인합니다. 그렇지 않으면 'uh_slp_proto_request()'가 호출됩니다.

uh_slp_proto_request_entrypoint

테스트를 수행하여 카메라에 몇 가지 요청을 보내면, 'uh_slp_proto_request()'에서 사용하는 데이터가 표준 POST 데이터임을 확인할 수 있습니다. 따라서 이전 페이로드와 함께 '/'에 POST 요청을 보내면, 'uh_slp_proto_request()'가 이 데이터를 처리하고, 'set_language()'를 호출하며, 우리의 페이로드가 'exec_and_get_result()'에 의해 실행되는 명령어에 주입됩니다.

보시다시피, 인증에 대해 언급하지 않았습니다. 'setLanguage()' 함수는 로그인 없이 호출될 수 있기 때문입니다. 이로 인해 모든 사용자가 인증 없이 단일 요청으로 카메라를 완전히 제어할 수 있습니다.

----[ 익스플로잇 ]----------------------------------

이제 익스플로잇을 작성할 시간입니다. netcat으로 역방향 셸(reverse shell)을 얻는 방법을 알아내는 데 시간을 투자했습니다. 간단해 보였지만 작동하지 않았습니다. BusyBox에 설치된 netcat 버전은 기능이 상당히 제한적이어서 일반적인 역방향 셸이 유효하지 않았습니다. 하지만 PayloadsAllTheThings 저장소(항상 그렇듯)에서 원하는 것을 찾았고 완벽한 역방향 셸을 얻었습니다. uhttpd가 root로 실행되기 때문에(TP-Link 덕분에), 악성 POST 요청을 보내기만 하면 최고 권한의 셸을 얻을 수 있습니다. 익스플로잇은 GitHub 페이지에서 확인할 수 있습니다: https://github.com/hacefresko/CVE-2021-4045/blob/master/pwntapo.py image

🚨 교훈:

  • https를 http로 착각하여 443 포트에서 시간을 낭비하다가 오류를 발견했습니다.
  • 2020, 554, 8800 포트의 서비스는 알려지지 않아 추가 조사가 필요했습니다.

🔓 셸 획득

UART를 통한 콘솔 액세스

  1. 사전 조사:
    • GitHub 저장소에서 장치의 리버스 엔지니어링 정보를 찾았습니다.
    • UART 포트에 접근하기 위해 USB-to-TTL 변환기 사용법을 배웠습니다.
도구 다운로드