Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2025-40634 — Exploit for stack-based buffer overflow found in the conn-indicator binary in the TP-Link Archer AX50 router | Kitploit
Tools/GitHubGitHub/hacefresko/cve-2025-40634
Embedded Systems SecurityIoT SecurityExploitationFuzzingPenetration TestingHardware SecurityRemote Access ToolBinary Exploitation
GitHubhacefresko/cve-2025-40634

CVE-2025-40634

Exploit for stack-based buffer overflow found in the conn-indicator binary in the TP-Link Archer AX50 router

View Repository
3181211 months agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2025-40634

The TP-Link Archer AX50 router is vulnerable to a stack-based buffer overflow on its firmware version 1.0.14 Build 20240108 rel.42655(4555), leading to Remote Code Execution both in the LAN and in the WAN side. This vulnerability has the same root cause as CVE-2020-10881, found by the Flashback team and largely detailed in their video series about it (check references). However, the exploitation process differs a bit, so a new exploit had to be written.

Root cause

This vulnerability occurs in the conn-indicator binary, which is in charge of checking if the router is connected to the internet by periodically sending DNS queries and listening for their responses at a random UDP port between 32000 and 61000.

The function that receives and first processes these DNS response packets is TPDns_RecvAndResolve(), located at 0x00405e3c. When receiving a packet with recvfrom, it is stored in buf, which has a size of 2960 bytes. Then, it checks that the return code is correct (RCODE == 0) and checks the numbers of questions and answers in the packet(QDCOUNT and ANCOUNT). In order to process the answers, it calls process_resolved_IP() and passes it a pointer to buf, answer_ptr, which is a pointer to where the answers are located inside the packet in buf, the number of answers (ANCOUNT) and other flags:

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;
          }

  [...]

Function process_resolved_IP(), located at 0x00405818, traverses each answer and calls DNS_answer_parser() for each one of them. It passes as arguments the same pointer to buf, answer, which is a pointer to the current answer being parsed inside the original packet at buf, and a pointer to current_answer, which is a buffer of size 256:

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 );
}

Function DNS_answer_parser(), located at 0x004054e0, parses each answer individually, which consists of a domain name represented as <len><domain><len><domain>.... As an example, example.com would be represented as 7example3com. The function loops through each pair of <len><domain> that constitute the domain name in the answer and checks that <len> is less than 63 (domain_name & 0xc0 != 0). Then, it calls memcpy and copies <len> bytes of answer, corresponding to <domain>, into current_answer. It repeats the process for the next pair of <len><domain> in the domain name.

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 );
}

Since current_answer is only 256 bytes long, it is possible for an attacker to send a packet with an answer containing a domain name large enough to overflow the buffer.

Exploitation

The exploitation process is very similar to the one explained by Flashback team for CVE-2020-10881 in their video series about it (see references), with some key differences:

  1. Addresses are not the same
  2. Code is not exactly the same, so offsets and the size of the stack in some moments was also not the same
  3. While the first and the third ROP gadgets where the same (located in different addresses), the second one differs in the move instruction, since now the register that is moved to $a2 as the count arg for memcpy() is $s1 instead of $s0.
  4. The i variable in process_resolved_IP() is loaded into $s5 instead of $s8. Then, when compared to $v1, $v1 is taken from $sp+616, which is very far from the overflowed buffer, so the stack had to be corrupted further from the answer and the command.
  5. The file system is read-only except for /tmp, so all writes and reads must be done there. This makes it impossible for the reverse shell to be less than 62 chars, so in order to deploy it to the target, it must be served from a web server and a command to download and execute it must be sent to the target.

With this in mind, I will explain the whole process anyways.

The conn-indicator binary has NX enabled and ASLR in everything but itself:

Download Tool