
Este repositório contém um post de blog sobre minha análise do CVE-2018-19987, uma injeção autenticada de comandos do SO que afeta vários roteadores D-Link.
Olá!
Neste pequeno post do blog, vou detalhar uma análise rápida que realizei para entender melhor os detalhes por trás do CVE-2018-19987. No começo, procurei esse CVE porque pensei que não houvesse nenhuma informação pública ou exploit. Pouco depois de fazer uma primeira análise, encontrei dois PoCs públicos no GitHub que você pode encontrar aqui e aqui.
Como já tinha feito a maior parte do trabalho e não tinha encontrado uma análise completa, decidi escrever o post de qualquer forma.
Espero que goste!
Atualização: Bem... depois de pesquisar no Google enquanto estava preso neste post do blog, encontrei o seguinte artigo que descreve uma vulnerabilidade muito semelhante (CVE-2018-19986) no D-Link DIR-818. Análise de vulnerabilidade de roteador série (5): Análise e reprodução de injeção de comando CVE-2018-19986 DIR-818LW&828 (traduzido pelo Google) por Ogur1.
Comecei minha análise reunindo todas as informações que pude sobre o dispositivo:
Nota: realizei toda a análise com o firmware v3.01B02.
As informações públicas para esta vulnerabilidade, disponíveis na página do Mitre, forneceram informações suficientes para começar:
dispositivos 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 e DIR-890L Rev.A 1.21B02_BETA manipulam incorretamente IsAccessPoint em /HNAP1/SetAccessPointMode. No código-fonte do SetAccessPointMode.php, o parâmetro IsAccessPoint é salvo no arquivo de script ShellPath sem qualquer verificação de regex. Após a execução do arquivo de script, ocorre a injeção de comando. Uma mensagem XML vulnerável do /HNAP1/SetAccessPointMode poderia conter metacaracteres de shell no elemento IsAccessPoint, como a string `telnetd`.
Para iniciar a análise, precisei de algo para examinar, então prossegui para extrair o sistema de arquivos do roteador.
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, o Binwalk detecta um sistema de arquivos Linux. Se olharmos mais atentamente as informações que temos, podemos ver que o aviso menciona o caminho /HNAP1. Portanto, podemos começar a examinar como o roteador lida com essas URLs.
Conforme explicado no excelente post do blog /dev/tty0, essas URLs no final são tratadas por um binário chamado cgibin, localizado em htdocs/cgibin. Mas eu queria entender melhor como isso estava configurado e, no final, me levou a ter que entender como o servidor HTTP estava configurado para processar solicitações. Procurei pelo nome do arquivo httpd.conf, assumindo que era usado para configurar o servidor HTTP, e encontrei um arquivo chamado HTTP.php na pasta /etc/services/, contendo, entre outras coisas, as seguintes linhas interessantes:
$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");
Presumi que em algum momento esse arquivo é executado e escreve o arquivo /var/run/httpd.conf. Para aprender como o servidor web estava configurado, prossegui para analisar o arquivo /etc/services/HTTP/httpcfg.php (incluí apenas as 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";
}
Agora temos todas as peças! Podemos assumir que o servidor web está configurado para lidar com solicitações HTTP para /HNAP1 usando o /usr/sbin/hnap, e com base no post do blog /dev/tty0 sabemos que será um link para o binário htdocs/cgibin.
O próximo passo foi dar uma olhada no binário cgibin para entender como ele lidava com solicitações HNAP. Na imagem a seguir podemos ver parte da função main descompilada, onde o caminho da URL passado para ela é comparado com diferentes caminhos (como session.cgi, authentication.cgi, captcha.cgi, para citar alguns) até que hnap seja encontrado e nossa função hnap_main seja chamada.
nota: Ao analisar a função main, tive alguns problemas para identificar a string hnap, pois o Ghidra não a detectou como string durante a análise inicial.
O próximo passo foi entender o que essa função fazia. Fornecerei apenas trechos de código para as partes relevantes que ajudarão a entender melhor o que a função faz, e também completarei outras partes relevantes com algum pseudocódigo — renomeei algumas variáveis para esclarecer seu 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);
}
...
Após concluir a análise desta função, decidi dar uma olhada no arquivo PHP SetAccessPointMode.php — que no final era o que continha a falha — e tentei juntar todas as peças:
...
$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, temos a variável $ShellPath que é preenchida pela função hnap_main e a variável $IsAccessPoint controlada pelo usuário e passada na solicitação XML enviada. Com isso, podemos corroborar como uma injeção de comando do sistema operacional na variável escrita pelo arquivo PHP é executada. Aqui você pode encontrar um PoC completo desenvolvido por pr0v3rbs para explorar este problema.
Discutiremos um pouco mais sobre as versões afetadas e corrigidas mais tarde. Como você pode ver na tabela abaixo, parece que houve algum tipo de regressão com o patch, que reintroduziu essa vulnerabilidade em versões que já deveriam ter sido corrigidas.
(*) Não verifiquei essas versões de firmware, pois suas contrapartes não criptografadas eram vulneráveis.
Como podemos ver na tabela acima, a vulnerabilidade foi corrigida em algumas versões de firmware removendo o arquivo PHP afetado SetAccessPointMode.php. Uma vez que o arquivo não existe, as verificações realizadas na função falharão e nada será executado. Confirmei isso analisando a hnap_function nas versões de firmware FW v3.131 e 3.15B02 WW; o código para hnap_main não mudou em relação a este problema, mas o arquivo PHP não estava mais presente.
O que é mais interessante é que, de alguma forma, esse bug foi reintroduzido na versão de firmware FW v3.11 até FW v3.12B04, depois de ter sido corrigido na versão 3.11B01_icjg_WW.
Como primeira conclusão, diria: nunca confie nesses dispositivos como um dispositivo verdadeiramente seguro — o que aconteceu com as atualizações é um exemplo claro de por que você não deveria fazer isso. Além disso, após a análise realizada, posso concluir que o primeiro PoC listado neste post do blog não funcionará para esta versão específica, pois vimos que existem algumas condições que devem ser cumpridas para executar o arquivo PHP vulnerável.
E a conclusão mais importante: tenha muito cuidado ao decidir o quão crítica é uma vulnerabilidade com base em sua pontuação CVSS. Decidi analisar este CVE porque foi classificado como 9.8 (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) e está claro que, pelo menos para esta versão, o invasor precisará de credenciais para poder explorá-la.
Como próximos passos, vou trabalhar para conseguir emular este binário para explorar essa vulnerabilidade sem ter acesso ao roteador em si, mas isso será material para outro post!
Obrigado por ler.
| Firmware | Versão | Hash MD5 | Data de lançamento | Vulnerável |
|---|
| DIR822C1_FW315WWb02.bin | 3.15B02 WW | 7121771c3e1706ba76fbf244023efad3 | 06/11/2019 | NÃO |
| DIR822C1_FW313WWb01.bin | FW v3.13 | aa16c7016f67be384e0784e439ce26d2 | 10/07/2019 | NÃO |
| DIR822C1_FW303WWb04_i4sa_middle.bin | FW v3.12B04 | c3b9a3f115c02e739690616aba2f2d99 | 26/04/2019 | SIM |
| DIR822C1_FW312WWb04.bin | FW v3.12B04 | eb11afbd136a5b29cea18141f727bfa8 | 26/04/2019 | SIM (*) |
| DIR822C1_FW311WWb01.bin | FW v3.11 | 6d7c90eaaae835667faea65c862b3c82 | 01/01/2019 | SIM |
| DIR822C1_FW311bWWb01_icjg.bin | 3.11B01_icjg_WW | 75e361e1465604aeda5d5dbcaecca977 | 21/12/2018 | NÃO |
| DIR822C1_FW303WWb04_i4sa_middle.bin | FW v3.10B06 | c3b9a3f115c02e739690616aba2f2d99 | 17/08/2018 | SIM |
| DIR822C1_FW310WWb06.bin | FW v3.10B06 | e33db75d0801fddb1c90308982e69fe5 | 17/08/2018 | SIM (*) |
| DIR-822_C1_FW302WWb05.bin | FW v3.02 | 0dbf840c0ff5d3a5b593d33690e15d82 | 14/09/2017 | SIM |
| DIR822C1_FW301WWb02.bin | FW v3.01B02 | 1bff7ec8b4da0643f65b4d44c630e92b | 27/04/2016 | SIM |