
Exploit de inyección de comandos para la cámara TP-Link Tapo C200 (CVE-2021-4045) que proporciona acceso a shell root a través de UART y análisis del binario uhttpd con ingeniería inversa.
CVE-2021-4045
CVE-2021-4045 es una vulnerabilidad de inyección de comandos descubierta en la cámara TP-Link Tapo C200, que permite a un atacante tomar el control total del dispositivo con privilegios de root. Esta vulnerabilidad afecta a todas las versiones de firmware anteriores a la 1.1.16 Build 211209 Rel. 37726N.
🔗 Aviso oficial de INCIBE: https://www.incibe.es/incibe-cert/alerta-temprana/vulnerabilidades/cve-2021-4045 (Reemplazar con el enlace real)
🔧 Solución recomendada: Actualizar el firmware a la versión 1.1.16 o superior.
$ nmap -sV -p- 192.168.1.81
Resultado:
PORT STATE SERVICE
443/tcp open https
554/tcp open rtsp
2020/tcp open xinupageserver
8800/tcp open sunwebadmin
Como pueden ver, el dispositivo tiene algunos puertos abiertos interesantes. Lo primero que probé fue el puerto 443. Aunque nmap indica claramente que usa https, cuando hice el escaneo inicial, lo pasé por alto y pasé bastante tiempo pensando que el puerto 443 estaba usando http. Debido a esto, solo probé http://192.168.1.81:443 en lugar de https://192.168.1.81:443, por lo que solo obtuve respuestas 400. Como dije en la introducción, este proceso estuvo lleno de fallos. En cuanto a los demás puertos, los servicios que se ejecutaban en ellos me eran totalmente desconocidos y no encontré ninguna información clara al respecto. En ese momento, me quedé sin opciones conocidas, así que era hora de investigar más a fondo.
----[ Obtener una shell ]-------------------------------
Antes de comprar la cámara, busqué en internet investigaciones previas sobre el dispositivo y, por suerte, encontré este repositorio de GitHub donde la gente colaboraba para realizar ingeniería inversa. Uno de los problemas explicaba cómo obtener acceso a la consola a través del puerto UART , algo que desconocía por completo en ese momento. Así que aprendí lo básico y compré un convertidor de USB a TTL para conectarme.
Con la ayuda del problema mencionado , pude abrir el dispositivo con un cuchillo y un destornillador y localizar rápidamente el UART. Después de un par de intentos y mucha paciencia, finalmente logré soldar algunos cables a las almohadillas.
Luego, llegó el momento de comprobar si la soldadura era lo suficientemente buena para la transmisión de datos. Conecté los cables al adaptador USB, teniendo en cuenta que Rx de la UART va a Tx del adaptador y viceversa, y conecté el adaptador a mi ordenador. De nuevo, gracias al problema mencionado , sabía que la velocidad de transmisión para la conexión serie era de 57600, así que ejecuté:
$ sudo screen /dev/tty.usbserial-0001 57600
Donde '/dev/tty.usbserial-0001' es el puerto USB al que está conectado el adaptador y que alimenta el dispositivo. Inmediatamente comencé a recibir datos, genial.
Sin embargo, aún no tenía acceso a la consola. Lo que recibía era simplemente la secuencia de arranque del dispositivo, que en realidad era el gestor de arranque U-Boot. Tenía un aspecto similar a este:
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
[...]
Al pulsar Enter, se nos pide que introduzcamos un nombre de usuario y una contraseña. Gracias a ese problema de GitHub , conocemos las credenciales, así que podemos iniciar sesión correctamente con el usuario 'root' y la contraseña 'slprealtek' y, finalmente, obtener acceso a la consola.
Una vez que comprobé que la conexión funcionaba, necesitaba reforzar la soldadura, ya que se había roto dos veces durante el proceso de montaje de la carcasa. Apliqué silicona termofusible para asegurar todos los cables y cerré el dispositivo, desconectando todos los motores. Ahora, mi unidad de prueba estaba lista.
----[ Explorando el dispositivo ]--------------------------
Ahora que tenemos una carcasa, exploremos el 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
Como podemos ver, se trata de una máquina OpenWRT que ejecuta Linux 3.10.27. Ahora vamos a comprobar los procesos activos y los puertos abiertos:
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
Podemos ver los procesos detrás de esos puertos abiertos que se ven en el escaneo de nmap, como uhttpd o cet. Me centré específicamente en el proceso uhttpd, ya que es el que está detrás del servidor https (que en ese momento todavía creía que era http) y ya estaba muy familiarizado con los protocolos http.
uhttpd es un servidor web creado por OpenWRT para su uso en dispositivos embebidos que ejecutan esta distribución. En este punto, quería saber si podía obtener más información al respecto, como el código fuente o, al menos, las rutas. Visité la wiki de OpenWRT y aprendí sobre uhttpd y OpenWRT en general. En las máquinas OpenWRT, existe un sistema llamado Interfaz de Configuración Unificada (UCI) , que se utiliza básicamente para configurar fácilmente los servicios del sistema. Usando esto, podemos obtener la configuración de 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
Aquí hay algunos parámetros interesantes. Primero, 'uhttpd.main.home' apunta al directorio raíz del servidor, por lo que podríamos encontrar algunos archivos del servidor web. Luego, 'uhttpd.main.lua_handler' apunta al script del controlador Lua que se usa para inicializar el entorno de ejecución de Lua al iniciar el servidor, ya que uhttpd admite scripts Lua, por lo que podría haber más archivos interesantes allí. Sin embargo, el directorio '/www' está vacío y no hay ningún directorio 'sgi' en '/usr/lib/lua/luci' ni el archivo 'uhttpd.lua' en el sistema. Intenté encontrar información sobre cómo funciona esta instancia de uhttpd, pero no encontré nada, solo parámetros de configuración que no apuntan a ninguna parte.
En este punto, supe que la solución era analizar directamente el binario 'uhttpd' y aplicar ingeniería inversa, pero antes quería crear un entorno de prueba para saber qué sucedía dentro del servidor web al realizar las solicitudes, ya que, por la forma en que se creó el proceso, no había ninguna salida en ningún sitio.
Intenté ejecutar el comando que se encuentra en la salida del comando ps para el proceso 861:
$ /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C
Sin embargo, obtuve muchos errores y no pude hacerlo funcionar. Como no pude crear el mismo proceso uhttpd en un puerto diferente, intenté buscar la salida faltante revisando la entrada '/proc' del proceso, para intentar leerlas si existían (como se explica en este video de PwnFunction ). Pero había un gran 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]
Todos los descriptores de archivo para 'stdin', 'stdout' y 'stderr' se redirigieron a '/dev/null', lo que básicamente los redirige a un agujero negro donde no se pueden encontrar. Estaba atascado y no sabía qué hacer. Como ya estaba en la entrada '/proc', empecé a investigar, ya que no recordaba que las entradas '/proc' tuvieran tanta información sobre un proceso y tenía curiosidad. Gracias a esta curiosidad fortuita, me topé con la entrada 'environ', que contiene todas las variables de entorno para ese proceso. Una de estas variables de entorno era:
UHTTPD_ARGS=-h /www -T 180 -A 0 -n 8 -R -r C200 -C /tmp/uhttpd.crt -K /tmp/uhttpd.key -s 443
Enseguida me di cuenta de que el comando mostrado por ps era incorrecto y, posteriormente, descubrí que se debía a que la interfaz UART no tenía suficiente ancho para mostrar todos los caracteres. Otro fallo más que me enseñó lecciones importantes: nunca hay que fiarse de la salida de un puerto UART.
Ahora por fin pude crear otra instancia de uhttpd con los mismos parámetros y sin tuberías a '/dev/null' para probar el binario mientras lo sometía a ingeniería inversa.
----[ Ingeniería inversa de uhttpd con Ghidra ]--------
Esta fue la primera vez que usé Ghidra. Había visto algunos videos y leído algunos artículos al respecto (gracias a stacksmashing y liveoverflow por el contenido increíble y fácil de entender), pero nunca lo había usado realmente, así que esta fue una muy buena oportunidad para aprender.
Abrí el binario uhttpd y, tras varios intentos, descubrí que el lenguaje era MIPS32, little endian, con mips16e. Algunos nombres de funciones venían por defecto con el binario, pero otros no. También dediqué algo de tiempo a renombrar funciones, ya que, al parecer, Ghidra suele confundirse con funciones externas y se obtienen envoltorios extraños para ellas, como:
Analicé la función `main()` y otras importantes para comprender la lógica del binario y su estructura. Encontré algunas interesantes, ya identificadas, entre las que se encontraban `do_login()` y `uh_slp_proto_request()`. Hablaré más sobre esta última más adelante.
Tras este primer contacto, empecé a buscar errores. Como soy un completo novato en vulnerabilidades de desbordamiento, lo primero que hice fue buscar llamadas a system(), exec() y popen() para comprobar si existía alguna vulnerabilidad de inyección de comandos que pudiera explotar fácilmente. Y vaya suerte tuve.
La función 'exec_and_read_json()' utiliza 'popen()' para ejecutar comandos:
ejecutar_y_leer_json
La función 'exec_and_read_json()' es utilizada por dos funciones sin nombre, a las que he denominado 'set_language()' y 'wifi_connect()'. Estas funciones se encargan respectivamente de la configuración del idioma y de la conexión Wi-Fi (obviamente). 'wifi_connect()' parece analizar las comillas simples ('), mientras que 'set_language()' no. Esto significa que si podemos controlar la entrada de la función 'set_language()', podemos inyectar nuestros propios comandos con éxito:
conexión wifi establecer_idioma
La función 'set_language()' es utilizada por 'uh_slp_proto_request()', la función que mencioné anteriormente, que pasa como entrada algunos datos analizados recibidos del usuario.
función_principal_1 función_principal_2
Para analizar los datos del usuario, uh_slp_proto_request() comprueba si se trata de un objeto JSON válido. A continuación, obtiene un valor de cadena identificado por la clave method y un valor de diccionario identificado por params (al menos eso creo, ya que Ghidra no pudo resolver la llamada a la función, pero parecía funcionar de esta manera). Dependiendo del método seleccionado, uh_slp_proto_request() selecciona la función que se ejecutará.
Entonces, al enviar la siguiente carga útil:
{"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.