
Este repositorio tiene una publicación de blog sobre mi análisis de CVE-2018-19987, una inyección de comandos del sistema operativo autenticada que afecta a múltiples routers D-Link.
¡Hola a todos!
En esta breve entrada de blog desglosaré un análisis rápido que realicé para comprender mejor los detalles detrás de CVE-2018-19987. Al principio fui tras este CVE porque pensé que no había información pública ni exploit. Poco después de hacer un primer análisis, encontré dos PoCs públicos en GitHub que puedes encontrar aquí y aquí.
Como ya había hecho la mayor parte del trabajo y no había encontrado un análisis completo, decidí escribir la entrada de todos modos.
¡Espero que lo disfrutes!
Actualización: Bueno... después de buscar en Google mientras estaba atascado con esta entrada de blog, encontré el siguiente artículo que describe una vulnerabilidad muy similar (CVE-2018-19986) en el D-Link DIR-818. Serie de análisis de vulnerabilidades de routers (5): análisis y reproducción de la vulnerabilidad de inyección de comandos CVE-2018-19986 DIR-818LW&828 (traducido por Google) por Ogur1.
Comencé mi análisis recopilando toda la información que pude sobre el dispositivo:
Nota: Realicé todo el análisis con el firmware v3.01B02.
La información pública de esta vulnerabilidad, disponible en la página de Mitre, proporcionó suficiente información para empezar:
D-Link DIR-822 Rev.B 202KRb06, DIR-822 Rev.C 3.10B06, DIR-860L Rev.B 2.03.B03, DIR-868L Rev.B 2.05B02, DIR-880L Rev.A 1.20B01_01_i3se_BETA, and DIR-890L Rev.A 1.21B02_BETA devices mishandle IsAccessPoint in /HNAP1/SetAccessPointMode. In the SetAccessPointMode.php source code, the IsAccessPoint parameter is saved in the ShellPath script file without any regex checking. After the script file is executed, the command injection occurs. A vulnerable /HNAP1/SetAccessPointMode XML message could have shell metacharacters in the IsAccessPoint element such as the `telnetd` string.
Para comenzar el análisis necesitaba algo que examinar, así que procedí a extraer el FS del router.
binwalk -eM DIR822C1_FW301WWb02.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 DLOB firmware header, boot partition: "dev=/dev/mtdblock/1"
10380 0x288C LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 4213444 bytes
1376372 0x150074 PackImg section delimiter tag, little endian size: 10505216 bytes; big endian size: 5021696 bytes
1376404 0x150094 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 5019773 bytes, 2282 inodes, blocksize: 131072 bytes, created: 2016-03-18 09:35:31
Como podemos ver, Binwalk detecta un sistema de archivos Linux. Si observamos más detenidamente la información que tenemos, podemos ver que el aviso menciona la ruta /HNAP1. Por lo tanto, podemos empezar a ver cómo el router maneja estas URLs.
Como se explica en la excelente entrada de blog de /dev/tty0, estas URLs terminan siendo manejadas por un binario llamado cgibin, ubicado en htdocs/cgibin. Pero quería comprender mejor cómo estaba configurado esto y, al final, me llevó a tener que entender cómo estaba configurado el servidor HTTP para procesar las solicitudes. Procedí a buscar el archivo httpd.conf, asumiendo que se usaba para configurar el servidor HTTP, y encontré un archivo llamado HTTP.php en la carpeta /etc/services/, que contenía, entre otras cosas, las siguientes líneas interesantes:
$httpd_conf = "/var/run/httpd.conf";
fwrite("a",$START, "xmldbc -P /etc/services/HTTP/httpcfg.php > ".$httpd_conf."\n");
fwrite("a",$START, "event PREFWUPDATE add /etc/scripts/prefwupdate.sh\n");
fwrite("a",$START, "httpd -f ".$httpd_conf."\n");
fwrite("a",$START, "event HTTP.UP\n");
fwrite("a",$START, "exit 0\n");
Supuse que en algún momento este archivo se ejecuta y escribe el archivo /var/run/httpd.conf. Para aprender cómo estaba configurado el servidor web, procedí a analizar el archivo /etc/services/HTTP/httpcfg.php (solo incluí las partes relevantes):
if ($hnap > 0)
{
echo
" Control". "\n".
" {". "\n".
" Alias /HNAP1". "\n".
" Location /htdocs/HNAP1". "\n".
" External". "\n".
" {". "\n".
" /usr/sbin/hnap { hnap }". "\n".
" }". "\n".
" IndexNames { index.hnap }". "\n".
" }". "\n";
}
¡Ahora tenemos todas las piezas! Podemos asumir que el servidor web está configurado para manejar las solicitudes HTTP a /HNAP1 mediante /usr/sbin/hnap, y según la entrada de blog de /dev/tty0 sabemos que será un enlace al binario htdocs/cgibin.
El siguiente paso fue echar un vistazo al binario cgibin para entender cómo manejaba las solicitudes HNAP. En la siguiente imagen podemos ver parte de la función main descompilada, donde la ruta URL que se le pasa se compara con diferentes rutas (como session.cgi, authentication.cgi, captcha.cgi, por nombrar algunas) hasta que se encuentra hnap y se llama a nuestra función hnap_main.
nota: Mientras analizaba la función main, tuve algunos problemas para identificar la cadena hnap, ya que Ghidra no la detectó como cadena durante el análisis inicial.
El siguiente paso era entender qué hacía esta función. Proporcionaré solo fragmentos de código de las partes relevantes que ayudarán a comprender mejor lo que hace la función; también completaré otras partes relevantes con algo de pseudocódigo —renombré algunas variables para aclarar su propósito.
...
HTTP_SOAPACTION = getenv("HTTP_SOAPACTION");
REQUEST_METHOD = getenv("REQUEST_METHOD");
HNAP_AUTH = getenv("HTTP_HNAP_AUTH");
__haystack = getenv("HTTP_COOKIE");
pcVar1 = getenv("HTTP_REFERER");
...
if (HTTP_SOAPACTION != "") {
if (HTTP_SOAPACTION == GetDeviceSettings) {
...
} else {
// These actions will occur during auth. Process
if ("GetCAPTCHAsetting" in HTTP_SOAPACTION) {
sess_generate_captcha();
} else {
if ("Login" in HTTP_SOAPACTION) {
perform_login();
}
// We'll land here once auth.
if (HNAP_AUTH != "") {
if ("uid=" in HTTP_COOKIE){
is_valid_auth = perform_auth_process()
if (is_valid_auth) {
if("logout" in HTTP_SOAPACTION){
perform_logout();
// If we are auth. and NOT trying to logout
// the code will try to perform the action
// we requested
} else {
// If we arrive here we win
// interesting code below
goto LAB_004141d4;
}
}
}
}
// If we are not authenticated we can't do anything
Return "You need proper authorization to use this resource"
}
}
} else {
...
}
LAB_004141d4:
hnap_action = get_hnap_operation(HTTP_SOAPACTION);
if (HTTP_SOAPACTION != "") {
hnap_action_len = strlen(hnap_action);
}
snprintf(path_to_hnap_php_file,0x100,"%s/%s.php","/etc/templates/hnap/",hnap_action);
if (!check_file_access(path_to_hnap_php_file)){
return "HNAP ACTION DOES NOT EXIST (FAIL)"
}
if (REQUEST_METHOD == "POST") {
// Here arguments for the PHP are extracted
parse_request_and_extract_xml()
if (hnap_action == "GetFirmwareStatus") {
system("sh /etc/events/checkfw.sh > /dev/console");
}
// Here the final arguments for the xmldbc_ephp are crafted
snprintf(ARGS_FOR_XMLDBC_PHP,0x100,"%s%s.php\nShellPath=%s%s.sh\nPrivateKey=%s\n",
"/etc/templates/hnap/", hnap_action, &ShellPath, hnap_action, &PRIVATE_KEY);
// PHP is executed (in our case SetAccessPointMode.php) and the shell
// script is written to ShellPath
xmldbc_ephp(0,0,ARGS_FOR_XMLDBC_PHP,stdout);
snprintf(hnap_action, 0x100, "%s", hnap_action);
// Shell command is built to run the previously written shell file
// (File written by the PHP script)
shell_command = "sh %s%s.sh > /dev/console &";
}
snprintf(cmd_to_execute, 0x100, shell_command, &PATH, hnap_action);
// File is executed containing the command injection
system(cmd_to_execute);
}
...
Una vez completado el análisis de esta función, decidí echar un vistazo al archivo PHP SetAccessPointMode.php — que al final era el que contenía el fallo — y traté de unir todas las piezas:
...
$IsAccessPoint = query("/runtime/hnap/SetAccessPointMode/IsAccessPoint");
...
fwrite("w",$ShellPath, "#!/bin/sh\n");
fwrite("a",$ShellPath, "echo [$0] $1 ... > /dev/console\n");
fwrite("a",$ShellPath, "echo IsAccessPoint = ".$IsAccessPoint." > /dev/console\n");
fwrite("a",$ShellPath, "echo Result = ".$Result."\n");
...
Como podemos ver, tenemos nuestra variable $ShellPath, que es rellenada por la función hnap_main, y la variable $IsAccessPoint, que está controlada por el usuario y se pasa en la solicitud XML enviada. Con esto podemos corroborar cómo se ejecuta una inyección de comandos del sistema operativo en la variable escrita por el archivo PHP. Aquí puedes encontrar un PoC completo desarrollado por pr0v3rbs para explotar este problema.
Hablaremos un poco más sobre las versiones afectadas y corregidas más adelante. Como puedes ver en la tabla siguiente, parece que hubo algún tipo de regresión con el parche, que reintrodujo esta vulnerabilidad en versiones que ya deberían haber sido parcheadas.
(*) No comprobé estas versiones de firmware, ya que sus contrapartes sin cifrar eran vulnerables.
Como podemos ver en la tabla anterior, la vulnerabilidad se corrigió en algunas versiones de firmware eliminando el archivo PHP afectado SetAccessPointMode.php. Una vez que el archivo no existe, las comprobaciones realizadas en la función fallarán y no se ejecutará nada. Confirmé esto analizando la hnap_function en las versiones de firmware FW v3.131 y 3.15B02 WW; el código de hnap_main no cambió en relación con este problema, pero el archivo PHP ya no estaba presente.
Lo más interesante es que, de algún modo, este error se reintrodujo en las versiones de firmware FW v3.11 hasta FW v3.12B04, después de haber sido parcheado en la versión 3.11B01_icjg_WW.
Como primera conclusión, diría que nunca confíes en estos dispositivos como dispositivos realmente seguros —lo que ocurrió con las actualizaciones es un claro ejemplo de por qué no deberías hacerlo. Además, tras el análisis realizado, puedo concluir que el primer PoC listado en esta entrada de blog no funcionará para esta versión específica, ya que vimos que hay algunas condiciones que deben cumplirse para ejecutar el archivo PHP vulnerable.
Y la conclusión más importante: ten mucho cuidado al decidir cuán crítica es una vulnerabilidad basándote en su puntuación CVSS. Decidí analizar este CVE porque tenía una puntuación de 9.8 (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) y está claro que, al menos para esta versión, el atacante necesitará credenciales para poder explotarla.
Como próximos pasos, trabajaré en poder emular este binario para explotar esta vulnerabilidad sin tener acceso al router en sí, ¡pero eso será material para otra entrada!
Gracias por leer.
| Firmware | Version | MD5 hash | Release date | Vulnerable |
|---|
| DIR822C1_FW315WWb02.bin | 3.15B02 WW | 7121771c3e1706ba76fbf244023efad3 | 06/11/2019 | NO |
| DIR822C1_FW313WWb01.bin | FW v3.13 | aa16c7016f67be384e0784e439ce26d2 | 10/07/2019 | NO |
| DIR822C1_FW303WWb04_i4sa_middle.bin | FW v3.12B04 | c3b9a3f115c02e739690616aba2f2d99 | 26/04/2019 | SÍ |
| DIR822C1_FW312WWb04.bin | FW v3.12B04 | eb11afbd136a5b29cea18141f727bfa8 | 26/04/2019 | SÍ (*) |
| DIR822C1_FW311WWb01.bin | FW v3.11 | 6d7c90eaaae835667faea65c862b3c82 | 01/01/2019 | SÍ |
| DIR822C1_FW311bWWb01_icjg.bin | 3.11B01_icjg_WW | 75e361e1465604aeda5d5dbcaecca977 | 21/12/2018 | NO |
| DIR822C1_FW303WWb04_i4sa_middle.bin | FW v3.10B06 | c3b9a3f115c02e739690616aba2f2d99 | 17/08/2018 | SÍ |
| DIR822C1_FW310WWb06.bin | FW v3.10B06 | e33db75d0801fddb1c90308982e69fe5 | 17/08/2018 | SÍ (*) |
| DIR-822_C1_FW302WWb05.bin | FW v3.02 | 0dbf840c0ff5d3a5b593d33690e15d82 | 14/09/2017 | SÍ |
| DIR822C1_FW301WWb02.bin | FW v3.01B02 | 1bff7ec8b4da0643f65b4d44c630e92b | 27/04/2016 | SÍ |