Share
## https://sploitus.com/exploit?id=98BE5734-F77A-5A56-9B28-1D46096CE816
# 🔍 **CVE-2021-4045: Vulnerabilidad de Inyección de Comandos en TP-Link Tapo C200**


>  CVE-2021-4045
---

## 📌 **Resumen**

**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](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**.

---



---

## 🛠 **Reconocimiento Inicial**

### Configuración y características del dispositivo
- Cámara IP económica (~30€) con funciones avanzadas:
  - Grabación en tarjeta SD.
  - Rotación horizontal de **360°** y vertical de **90°**.
  - Reproducción de audio en tiempo real desde la app móvil.

### Escaneo de puertos
```bash
$ 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 /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**:
- Confundí `https` con `http` en el puerto **443**, perdiendo tiempo hasta descubrir el error.
- Los servicios en los puertos **2020**, **554** y **8800** eran desconocidos, lo que requirió investigación adicional.

---

## 🔓 **Obtención de una Shell**

### Acceso a la consola mediante UART
1. **Investigación previa**:
   - Encontré un repositorio de GitHub con información sobre ingeniería inversa del dispositivo.
   - Aprendí a usar un **convertidor USB a TTL** para acceder al puerto **UART**.