Hexo
某云盘分析
Posted on: 2024-01-09 Edited on: 2024-03-14 In:  Views: 

1.攻击面分析

扫描所有的TCP端口

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
$ nmap -A -p- 192.168.124.14
Starting Nmap 7.93 ( https://nmap.org ) at 2023-08-13 19:53 CST
Nmap scan report for 192.168.124.14
Host is up (0.035s latency).
Not shown: 65522 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.16.1
|_http-server-header: nginx/1.16.1
|_http-title: 403 Forbidden
111/tcp open rpcbind 2 (RPC #100000)
| rpcinfo:
| program version port/proto service
| 100000 2 111/tcp rpcbind
|_ 100000 2 111/udp rpcbind
139/tcp open netbios-ssn Samba smbd 3.X - 4.X (workgroup: WORKGROUP)
443/tcp open ssl/http nginx 1.16.1
|_http-server-header: nginx/1.16.1
| tls-nextprotoneg:
|_ http/1.1
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Not valid before: 2021-11-01T08:48:38
|_Not valid after: 2021-12-01T08:48:38
| tls-alpn:
|_ http/1.1
|_http-title: 403 Forbidden
445/tcp open netbios-ssn Samba smbd 4.6.11 (workgroup: WORKGROUP)
1883/tcp open mqtt?
| mqtt-subscribe:
|_ ERROR:
6900/tcp open http nginx 1.16.1
|_http-title: 502 Bad Gateway
|_http-server-header: nginx/1.16.1
6901/tcp open http nginx 1.16.1
|_http-server-header: nginx/1.16.1
|_http-title: Site doesn't have a title (text/debug).
7000/tcp open http nginx 1.16.1
|_http-title: Site doesn't have a title (text/debug).
|_http-server-header: nginx/1.16.1
16800/tcp open ssl/http nginx 1.16.1
| tls-nextprotoneg:
|_ http/1.1
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.16.1
|_http-title: 502 Bad Gateway
| ssl-cert: Subject: organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Not valid before: 2021-11-01T08:48:38
|_Not valid after: 2021-12-01T08:48:38
| tls-alpn:
|_ http/1.1
16900/tcp open ssl/http nginx 1.16.1
| tls-nextprotoneg:
|_ http/1.1
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Not valid before: 2021-11-01T08:48:38
|_Not valid after: 2021-12-01T08:48:38
| tls-alpn:
|_ http/1.1
|_http-server-header: nginx/1.16.1
|_http-title: 502 Bad Gateway
17000/tcp open ssl/http nginx 1.16.1
|_http-server-header: nginx/1.16.1
| tls-alpn:
|_ http/1.1
| tls-nextprotoneg:
|_ http/1.1
|_http-title: Site doesn't have a title (text/debug).
| ssl-cert: Subject: organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Not valid before: 2021-11-01T08:48:38
|_Not valid after: 2021-12-01T08:48:38
|_ssl-date: TLS randomness does not represent time
20223/tcp open unknown
Service Info: Host: N1CLOUD

Host script results:
| smb2-time:
| date: 2016-09-30T16:08:46
|_ start_date: N/A
| smb-security-mode:
| account_used: guest
| authentication_level: user
| challenge_response: supported
|_ message_signing: disabled (dangerous, but default)
| smb-os-discovery:
| OS: Windows 6.1 (Samba 4.6.11)
| Computer name: n1cloud
| NetBIOS computer name: N1CLOUD\x00
| Domain name: \x00
| FQDN: n1cloud
|_ System time: 2016-10-01T00:08:42+08:00
|_nbstat: NetBIOS name: N1CLOUD, NetBIOS user: <unknown>, NetBIOS MAC: 000000000000 (Xerox)
| smb2-security-mode:
| 311:
|_ Message signing enabled but not required
|_clock-skew: mean: -2507d22h26m57s, deviation: 4h37m07s, median: -2507d19h46m58s

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 163.26 seconds

由于UDP端口扫描太慢,所以暂且就不分析了,由TCP扫描的结果可以看出,开放了许多大端口,也就是有相当多的自定义服务,给我们提供了相当多的攻击面。

2.固件获取

固件获取的方法通常为官网下载,但对于这款设备,我们无法从官网上获得其固件更新包,这种情况下我们一般有两种选择:抓取升级流量和通过串口提取固件。

3.固件分析

使用binwalk查看固件

一个很典型的嵌入式Linux类别的固件,binwalk显示了Linux内核映像以及设备树,最后是squashfs的文件系统。

然后直接binwalk -Me一把梭

image-20230809145718454

得到这样的结果,直接去看squashfs-root这个文件夹,因为这里面存储的就是设备运行的各个文件

image-20230809145835310

ls可以看到非常多的文件夹,那么这么多的文件夹/文件,我们该从哪里入手?

嵌入式Linux的启动流程

这里就需要谈一谈嵌入式Linux的启动流程了

image-20230809150757692

上面是一个嵌入式Linux设备的典型架构,固件最开始是bootloader,然后跟着bootloader的启动参数,之后就是kernel以及文件系统。

我上面图中的bootloader实际上包含两个部分:bootloader和boot,bootloader用于引导执行boot,这个boot可以是厂商自己设计的,也可以是使用的开源的boot,比如u-boot。u-boot在系统上电以后,他会完成下面几个重要的任务:

  • 初始化相关的硬件组件
  • 初始化系统内存,准备将系统的控制权交给相应的系统
  • 分配系统的资源
  • 提供相应的机制,用于定位和加载系统镜像
  • 加载操作系统,并将控制权移交给操作系统

这个过程其实就是一些底层硬件初始化的工作,包括包括串行通信端口的配置,内部时钟配置以及一些子硬件系统(I2C、DRAM、Flash、网络子系统等)的配置。另外,U-boot有一个控制台模式,它会等待用户一段时间,等待用户给他一个信息(回车),让他进入控制台模式。如果等待的时间过了用户没有任何操作,系统就继续往下走了。

在U-boot完成初始化工作以后,它剩下的唯一工作就是加载并启动linux操作系统内核。当U-boot开始加载指定物理地址的操作系统内核以后,U-boot引导装入程序的使命就到此结束了,接下来系统由linux内核接管。

当linux内核开始启动以后,开始打印各种设备信息,此时做的工作就是对各个外设进行驱动初始化,让他们各就各位。在执行完这些操作以后就要开始挂载根文件系统了。

什么是文件系统?

我们在Linux下看到的目录结构

image-20230809153031767

实际上就是文件系统。

我们所有的文件都是存储在磁盘中的数据,但如果让我们人工直接去磁盘里面找数据那就有点难为人了。因此文件系统是被开发用于管理磁盘的软件系统,文件在磁盘中的存储位置,寻找方式以及属性等等都由文件系统决定。功能这么重要的软件自然不会只有一种,不同操作系统不同场景使用的文件系统都有差别。

在我们使用的Linux机器中,一般使用ext3和ext4的文件系统:

image-20230809154912300

ext4是基于磁盘的文件系统,但我们的嵌入式设备通常是用不到磁盘来进行文件存储的,嵌入式设备使用的存储设备一般闪存(flash),闪存主要有NOR和NAND两种技术。在顺手介绍下NOR FLASH和NAND FLASH的区别:

NOR Flash的读取和我们常见的SDRAM的读取是一样,用户可以直接运行装载在NOR FLASH里面的代码,这样可以减少SRAM的容量从而节约了成本;NAND Flash没有采取内存的随机读取技术,它的读取是以一次读取一块的形式来进行的,通常是一次读取512个字节,采用这种技术的Flash比较廉价。用户不能直接运行NAND Flash上的代码,因此好多使用NAND Flash的开发板除了使用NAND Flash以外,还加上了一块小的NOR Flash来运行启动代码。

由于Flash存储器的擦写次数是有限的,NAND闪存还有特殊的硬件接口和读写时序。因此,必须针对Flash的硬件特性设计符合应用要求的文件系统;传统的文件系统如ext2等,用作Flash的文件系统会有诸多弊端。

针对于flash的文件系统也有很多类别:

  • jffs2:主要用于NOR型闪存,缺点主要是当文件系统已满或接近满时,因为垃圾收集的关系而使jffs2的运行速度大大放慢。
  • yaffs/yaffs2:用于NAND型闪存,yaffs与yaffs2的主要区别在于,前者仅支持小页(512 Bytes) NAND闪存,后者则可支持大页(2KB) NAND闪存。同时,yaffs2在内存空间占用、垃圾回收速度、读/写速度等方面均有大幅提升。
  • squashfs:无论是Nor Flash还是NAND Flash,SquashFS都可以在其上运行。

其中squashfs这个文件系统使用的是最广的,在我们要分析的这个固件中使用的也是squashfs。

继续看到嵌入式设备启动流程,前面讲到了内核挂载了文件系统。

内核文件加载以后,就开始运行第一个程序 /sbin/init,它的作用是初始化系统环境.

image-20230809172853758

在squashfs的根目录下可以看到init程序是指向sbin目录下的init的。由于init是第一个运行的程序,它的进程编号(pid)就是1。其他所有进程都从它衍生,都是它的子进程。

在Linux下有许多开机就要运行的守护进程(daemon),init的一大作用就是去运行这些开机启动的程序,init会去读取/etc/inittab这个文本文件,在这个设备中如下

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# /etc/inittab init(8) configuration for BusyBox
#
# Copyright (C) 1999-2004 by Erik Andersen <andersen@codepoet.org>
#
#
# Note, BusyBox init doesn't support runlevels. The runlevels field is
# completely ignored by BusyBox init. If you want runlevels, use sysvinit.
#
#
# Format for each entry: <id>:<runlevels>:<action>:<process>
#
# <id>: WARNING: This field has a non-traditional meaning for BusyBox init!
#
# The id field is used by BusyBox init to specify the controlling tty for
# the specified process to run on. The contents of this field are
# appended to "/dev/" and used as-is. There is no need for this field to
# be unique, although if it isn't you may have strange results. If this
# field is left blank, it is completely ignored. Also note that if
# BusyBox detects that a serial console is in use, then all entries
# containing non-empty id fields will _not_ be run. BusyBox init does
# nothing with utmp. We don't need no stinkin' utmp.
#
# <runlevels>: The runlevels field is completely ignored.
#
# <action>: Valid actions include: sysinit, respawn, askfirst, wait, once,
# restart, ctrlaltdel, and shutdown.
#
# Note: askfirst acts just like respawn, but before running the specified
# process it displays the line "Please press Enter to activate this
# console." and then waits for the user to press enter before starting
# the specified process.
#
# Note: unrecognised actions (like initdefault) will cause init to emit
# an error message, and then go along with its business.
#
# <process>: Specifies the process to be executed and it's command line.
#
# Note: BusyBox init works just fine without an inittab. If no inittab is
# found, it has the following default behavior:
# ::sysinit:/etc/init.d/rcS
# ::askfirst:/bin/sh
# ::ctrlaltdel:/sbin/reboot
# ::shutdown:/sbin/swapoff -a
# ::shutdown:/bin/umount -a -r
# ::restart:/sbin/init
#
# if it detects that /dev/console is _not_ a serial console, it will
# also run:
# tty2::askfirst:/bin/sh
# tty3::askfirst:/bin/sh
# tty4::askfirst:/bin/sh
#
# Boot-time system configuration/initialization script.
# This is run first except when booting in single-user mode.
#
::sysinit:/bin/hostname -F /etc/hostname
::sysinit:/etc/init.d/rcS


# /bin/sh invocations on selected ttys
#
# Note below that we prefix the shell commands with a "-" to indicate to the
# shell that it is supposed to be a login shell. Normally this is handled by
# login, but since we are bypassing login in this case, BusyBox lets you do
# this yourself...
#
# Start an "askfirst" shell on the console (whatever that may be)
# ::askfirst:-/bin/sh
# Start an "askfirst" shell on /dev/tty2-4
# tty2::askfirst:-/bin/sh
# tty3::askfirst:-/bin/sh
# tty4::askfirst:-/bin/sh

# /sbin/getty invocations for selected ttys
# tty4::respawn:/sbin/getty 38400 tty5
# tty5::respawn:/sbin/getty 38400 tty6

# Example of how to put a getty on a serial line (for a terminal)
::respawn:/sbin/getty -L ttyS000 115200 vt100 -n root -I "Auto login as root"
#::respawn:-/bin/sh
#
# Example how to put a getty on a modem line.
#::respawn:/sbin/getty 57600 ttyS2

# Stuff to do when restarting the init process
::restart:/sbin/init

# Stuff to do before rebooting
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
::shutdown:/sbin/swapoff -a

inittab文件中的值都是如下格式:

1
label:runlevel:action:process

比如这两行

1
2
::sysinit:/bin/hostname -F /etc/hostname
::sysinit:/etc/init.d/rcS

在系统初始化时就运行hostname程序,并且运行/etc/init.d/rcS这个程序。

接下来让我们看到/etc/init.d/rcS这个程序

rcS实际上是一个shell脚本

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
#!/bin/sh

# Mount fs accroding to /etc/fstab
mount -a

LDCONFIG=$(type -p ldconfig)
if [ -f "${LDCONFIG}" ]; then
${LDCONFIG} -C /tmp/ld.so.cache
fi

for initscript in /etc/init.d/S[0-9][0-9]*
do
if [ -x $initscript ] ;
then
$initscript
fi
done

if [ -f /hybroad/local.rc ]; then
echo "start user local.rc"
/bin/sh /hybroad/local.rc
fi

#Start user service if it exists
if [ -f /root/rc.user ]; then
echo "start user services"
/bin/sh /root/rc.user
fi

rcS会执行/etc/init.d/下的所有以S开头的脚本文件

image-20230809180234370

然后又会执行/hybroad/local.rc这个文件,注意到,hybroad这个目录并不是Linux的原生目录,应该是这个设备为了实现其功能而自定义的目录,需要重点关注。

local.rc的内容如下

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/bin/sh

# Export ENV
export TMPDIR=/var
export LD_LIBRARY_PATH="/lib:/usr/local/lib:/usr/lib:/usr/share/bluetooth/lib:/hybroad/lib"
export PATH="/usr/bin:/usr/sbin:/bin:/sbin:/hybroad/bin"
export TERM='vt100'
export TERMINFO='/usr/share/terminfo'

echo 64 > /proc/sys/kernel/msgmni
echo 1 > /proc/sys/kernel/auto_msgmni
echo 204800 > /proc/sys/net/core/rmem_max

if [ -z $RUNLEVEL ]; then
RUNLEVEL=4
fi

echo RUNLEVEL = $RUNLEVEL
if [ $RUNLEVEL = 4 ]; then
# Mount /var
echo "Mount var as 80M"
mount -nt tmpfs -o size=80M,mode=777 tmpfs /var
cd /var
mkdir lib lib/bluetooth lock log run tmp dhcpc empty empty/sshd db
touch run/utmp log/wtmp
chmod 755 empty
cd -
fi

partition_index roothome
ROOTHOME=$?
if [ "$ROOTHOME" -ne 255 ]; then
echo "roothome=$ROOTHOME"
else
partition_index cache
ROOTHOME=$?
if [ "$ROOTHOME" -ne 255 ]; then
echo "cache=$ROOTHOME"
else
ROOTHOME=255
echo "roothome=$ROOTHOME"
fi
fi
if [ "$ROOTHOME" -ne 255 ]; then
mount -t ext4 /dev/mmcblk0p$ROOTHOME /root
if [ "$?" != "0" ]; then
mke2fs -t ext4 -F /dev/mmcblk0p$ROOTHOME
sleep 2
mount -t ext4 /dev/mmcblk0p$ROOTHOME /root
echo "mount -t ext4 /dev/mmcblk0p$ROOTHOME /root"
fi
ROOTSIZE=`df | grep /dev/mmcblk0p$ROOTHOME | cut -b 45-50`
fi

mount -t ext4 /dev/mmcblk0p17 /storage
if [ $? -ne 0 ]; then
mkfs.ext4 -T largefile -F /dev/mmcblk0p17 -q
fi
mount -t ext4 /dev/mmcblk0p17 /storage



sleep 1
WIFIID=`lsusb | awk -F'ID ' '{print $2}' | fgrep "0e8d"`
echo "$WIFIID"
if [ "$WIFIID" == "0e8d:76a0" ] || [ "$WIFIID" == "0e8d:7662" ]; then
echo "insmod /kmod/mt7662u_sta.ko"
insmod /kmod/mt7662u_sta.ko
if [ -d /sys/class/net/wlan0 ]; then
touch /tmp/wifi_support_5G.txt
fi
else
echo "No Internal WIFI Device"
fi

if [ -x /hybroad/bin/usbmounter ]; then
/hybroad/bin/usbmounter &
fi

ifconfig eth0 up
ifconfig wlan0 up

touch /root/.reboot
cnt=`cat /root/.reboot`
if [ -z ${cnt} ]; then
cnt=0
fi

cnt=`expr ${cnt} + 1`
echo $cnt > /root/.reboot
ifconfig | grep wlan0
if [ $? != 0 ]; then
echo "###############################REBOOT##############################"${cnt}
echo "###############################REBOOT##############################"${cnt}
echo "###############################REBOOT##############################"${cnt}
echo "###############################REBOOT##############################"${cnt}
echo "###############################REBOOT##############################"${cnt}

if [ ${cnt} -lt 10 ]; then
sleep 1
reboot -f
else
echo "[ERROR]#######cnt > 10, give up reboot, wlan0 is not up#########"
fi
fi
echo "0" > /root/.reboot

mac=`ifconfig eth0 | grep HWaddr | awk {'print $5'}`
device_search_server "mac:"${mac} &

if [ $RUNLEVEL = 4 ]; then
echo "Extract MicroHei.ttf to /var"
tar xzf /hybroad/share/MicroHei.ttf.tar.gz -C /var
# Start telnetd or sshd service
if [ -x /usr/sbin/sshd ]; then
echo "Start ssh service ..."
/usr/sbin/sshd
else
echo "Can not find ssh server, then use telnet server ..."
telnetd 2>/dev/null
fi
# Back upgrade tool
if [ -x /hybroad/bin/back_upgrade.elf ]; then
/hybroad/bin/back_upgrade.elf &
fi
fi

if test -n $RUNLEVEL ; then
cd /hybroad/bin
while [ 1 ]; do
case $RUNLEVEL in
4)
echo Starting factory test ...
if [ -f /root/eWindow/eWindow.elf ]; then
/root/eWindow/eWindow.elf
else
/hybroad/bin/eWindow.elf
fi
;;
*)
if [ -x /hybroad/user.rc ]; then
echo Starting user.rc ...
/hybroad/user.rc
else
ifconfig eth0 10.0.0.188 broadcast 10.255.255.255 netmask 255.0.0.0
# Mount /var
echo "Mount var as 80M"
mount -nt tmpfs -o size=80M,mode=777 tmpfs /var
cd /var
mkdir lib lib/bluetooth lock log run tmp dhcpc empty empty/sshd db
touch run/utmp log/wtmp
chmod 755 empty
cd -
# Extract MicroHei.ttf
echo "Extract MicroHei.ttf to /var"
tar xzf /hybroad/share/MicroHei.ttf.tar.gz -C /var
# Start telnetd or sshd service
if [ -x /usr/sbin/sshd ]; then
echo "Start ssh service ..."
/usr/sbin/sshd
else
echo "Can not find ssh server, then use telnet server ..."
telnetd 2>/dev/null
fi
fi
break;
;;
esac
done
fi

在这个脚本中也启动了更多的进程,如下

1
2
3
if [ -x /hybroad/user.rc ]; then
echo Starting user.rc ...
/hybroad/user.rc

local.rc顺便启动了/hybroad/user.rc,user.rc用于启动用户服务,由于user.rc内容太长,在这里只截取一些我们感兴趣的部分

image-20230809181436931

user.rc中启动了nginx服务。

什么是nginx?

Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器。

HTTP服务器我们都知道是什么,那么什么是反向代理web服务器?

首先,有反向就肯定有正向,我们先来看看正向代理服务器。

1

正向代理: 我们平时需要访问国外的浏览器是不是很慢,比如我们要看推特,看GitHub等等。我们直接用国内的服务器无法访问国外的服务器,或者是访问很慢。所以我们需要在本地搭建一个服务器来帮助我们去访问。那这种就是正向代理。正向代理“代理”的是客户端,而且客户端是知道目标的,而目标是不知道客户端是通过VPN访问的。

再看到反向代理:

2

反向代理:那什么是反向代理呢。比如:我们访问淘宝的时候,淘宝内部肯定不是只有一台服务器,它的内部有很多台服务器,那我们进行访问的时候,因为服务器中间session不共享,那我们是不是在服务器之间访问需要频繁登录,那这个时候淘宝搭建一个过渡服务器,对我们是没有任何影响的,我们是登录一次,但是访问所有,这种情况就是 反向代理。反向代理“代理”的是服务端,对于客户端是透明的,客户端是不知道服务器内部使用了代理的,我们只需要将数据发送给服务器,由服务端进行选择即可。

而nginx就是实现反向代理的一个服务器,能够将不同的数据转发给不同的服务进行处理。

nginx的配置文件为nginx.conf,路径为/usr/local/nginx/conf,这个配置文件一共由三部分组成,分别为全局块、events块和http块。在http块中,又包含http全局块、多个server块。每个server块中,可以包含server全局块和多个location块。在同一配置块中嵌套的配置块,各个之间不存在次序关系。

大致结构如下

image-20230809184814436

我们重点关注http块

image-20230810122954192

这里定义了http服务

image-20230810123051866

定义了https服务

image-20230810123256608

在这里监听了一些自定义端口,6900,6901。用于处理baiduyunpan.com域名下的请求。

该服务器块监听端口为6900,当收到请求时,根据请求路径进行不同的处理。

  • /fsnotify:当请求路径为/fsnotify时,会执行FastCGI处理。使用include指令引入fastcgi_params配置文件,指定fastcgi_pass指令将请求转发给Unix域套接字/tmp/fsnotify.sock上的FastCGI后端。
  • /eshell:当请求路径为/eshell时,同样执行FastCGI处理。使用include指令引入fastcgi_params配置文件,指定fastcgi_pass指令将请求转发给Unix域套接字/tmp/http_eshell.sock上的FastCGI后端。
  • /baiduyunpan:当请求路径为/baiduyunpan时,执行FastCGI处理。同样使用include指令引入fastcgi_params配置文件,指定fastcgi_pass指令将请求转发给Unix域套接字/tmp/baiduyunpan.sock上的FastCGI后端。
  • /:当请求路径不匹配以上任何规则时,执行默认的FastCGI处理。同样使用include指令引入fastcgi_params配置文件,指定fastcgi_pass指令将请求转发给Unix域套接字/tmp/baiduyunpan.sock上的FastCGI后端。

我们看看fsnotify.sock上定义的后端是什么

使用如下方法命令进行全局查找

1
2
3
4
squashfs-root$ grep -r "fsnotify.sock"
usr/local/nginx/conf/nginx.conf: fastcgi_pass unix:/tmp/fsnotify.sock;
usr/local/nginx/conf/nginx.conf: fastcgi_pass unix:/tmp/fsnotify.sock;
bin/fsnotify_guard.sh: spawn-fcgi -n -s /tmp/fsnotify.sock -f "/usr/sbin/fsnotify_server $1 $2"

可以看到除了nginx.conf之中使用了fsnotify.sock,在fsnotify_guard.sh中也引用了fsnotify.sock,我们继续查找fsnotify_guard.sh在何处被调用了

1
2
squashfs-root$ grep -r "fsnotify_guard.sh"
usr/sbin/mqtt_run.sh:fsnotify_guard.sh ${name} ${psw} &

mqtt_run.sh之中调用了fsnotify_guard.sh

而在user.rc中

image-20230810124131527

调用了mqtt_run.sh

从上面的分析可以看出,访问6900端口的/fsnotify路径会给到/usr/sbin/fsnotify_server进行处理。

同理,/eshell,/baiduyunpan也都有对应的server进行处理,这些服务在user.rc中就启动了。

image-20230810125906670

到现在我们就大致理清了这个设备的运行流程:以nginx作为服务器,用户通过手机app控制云盘,不同的功能发包的目标端口不同,且访问路径不同,nginx通过访问路径和端口号来决定要使用哪个服务来处理访问请求。

我们可以通过burp抓包来确认自己的想法,毕竟这只是纯粹的静态分析,正不正确还不一定。

image-20230814145646572

image-20230814145702905

image-20230814145739491

4.漏洞挖掘

由于服务众多,本着自定义服务都可以挖的原则,直接从第一个fsnotify_server下手

main函数长这样

image-20230810131632105

开头这几个mosquitto开头的函数,是mosquitto这个工具的api,mosquitto是什么?

Mosquitto是一个开源的MQTT消息代理(broker),用于支持MQTT协议的消息传递。

那么,MQTT是什么?

MQTT(Message Queuing Telemetry Transport)是一种轻量级的发布/订阅消息传递协议,最初由IBM开发,并广泛用于物联网(IoT)应用中的设备间通信。它的设计目标是在低带宽和不稳定网络环境下,提供高效的消息传递机制。

其大致架构如下

3

发布者发布某个主题的消息,订阅者订阅某个主题的消息。

举个例子,比如发布者想要发布主题为“温度”的消息,如果订阅者想要接收到发布者发布的温度信息,那么就需要先在broker中订阅温度主题,这样当主题为温度的数据发送给broker时,broker就会将温度的信息转发给所有订阅了温度主题的订阅者。

1
2
3
4
5
6
mosquitto_new用于创建一个新的mqtt客户端
mosquitto_username_pw_set用于设置Mosquitto客户端连接到MQTT代理所需的用户名和密码
mosquitto_connect_callback_set用于设置当客户端成功连接到MQTT代理时的回调函数
mosquitto_message_callback_set用于设置当接收到消息时的回调函数
mosquitto_connect用于连接Mosquitto客户端到指定的MQTT代理
mosquitto_loop_start 是一个 Mosquitto 库函数,用于启动一个线程,使 Mosquitto 客户端实例可以在后台循环处理网络和消息事件

main函数整体如下

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
size_t v3; // r0
size_t v4; // r0
pthread_t newthread; // [sp+10h] [bp-24h] BYREF
_DWORD *i; // [sp+14h] [bp-20h]
char *newa; // [sp+18h] [bp-1Ch]
int v9; // [sp+1Ch] [bp-18h]
FILE *v10; // [sp+20h] [bp-14h]
char *s; // [sp+24h] [bp-10h]
int v12; // [sp+28h] [bp-Ch]
FILE *stream; // [sp+2Ch] [bp-8h]

sub_12FF4();
mosq = mosquitto_new(0, 1, 0);//创建一个新的mqtt客户端
mosquitto_username_pw_set(mosq, a2[1], a2[2]);//设置连接用户名和密码
mosquitto_connect_callback_set(mosq, sub_14988);//设置连接成功时的回调函数
mosquitto_message_callback_set(mosq, sub_147F0);//设置接收到消息时的回调函数
if ( mosquitto_connect(mosq, "127.0.0.1", 1883, 120) )//连接mqtt代理
{
if ( off_44A8C )
{
stream = (FILE *)fopen64(off_44A8C, "a+");
if ( stream )
{
fprintf(
stream,
"time:%s, file:%s, line:%d, func:%s, info:mqtt connect failed\n",
"Wed Oct 19 09:29:28 2022",
"server.c",
1716,
"main");
v12 = ftell(stream);
fclose(stream);
if ( v12 > 0x800000 )
{
v3 = strlen(off_44A8C);
s = (char *)malloc(v3 + 2);
sprintf(s, "%s0", off_44A8C);
rename(off_44A8C, s);
}
}
}
}
else if ( mosquitto_loop_start(mosq) )
{
if ( off_44A8C )
{
v10 = (FILE *)fopen64(off_44A8C, "a+");
if ( v10 )
{
fprintf(
v10,
"time:%s, file:%s, line:%d, func:%s, info:mosq_loop_start failed\n",
"Wed Oct 19 09:29:28 2022",
"server.c",
1721,
"main");
v9 = ftell(v10);
fclose(v10);
if ( v9 > 0x800000 )
{
v4 = strlen(off_44A8C);
newa = (char *)malloc(v4 + 2);
sprintf(newa, "%s0", off_44A8C);
rename(off_44A8C, newa);
}
}
}
}
else
{
pthread_create(&newthread, 0, (void *(*)(void *))start_routine, 0);
}
for ( i = sub_2E21C(64, (int)sub_19600, (int)sub_19758, 0); ; sub_2E2AC(i, -1) )//最后得执行这个奇怪的for循环
;
}

看到sub_2E21C函数

image-20230810150210199

都是赋值的操作,意义不明,只看到最后一个FCGX_Init函数。

再看到sub_19600函数

image-20230810152643683

也使用了FCGX_FPrintF。

谷歌一下。

image-20230810151240397

下载源码编译

1
2
3
4
mkdir install
./configure --prefix=./install
make
make install

image-20230810153307313

然后生成这3个目录,我们看到lib目录

image-20230810153357415

用IDA打开libfcgi.so.0.0.0这个文件

image-20230810153916041

然后生成C头文件

然后再载入头文件

image-20230810154351851

image-20230810154506619

可以看到导入了不少结构体,接下来我们来恢复一下函数的参数,方便我们查看

比如这个FCGX_FPrintF,第一个参数看起来是个结构体,

image-20230810155052528

其函数原型如下

1
int FCGX_FPrintF(FCGX_Stream *stream, const char *format, ...)

还有下面的

image-20230811123209846

函数原型如下

1
2
char *FCGX_GetParam(const char *name, FCGX_ParamArray envp)
int FCGX_GetStr(char *str, int n, FCGX_Stream *stream);

但是注意到在伪代码中FCGX_GetParam和FCGX_GetStr的参数都是a1+x的形式,也就是说FCGX_GetParam和FCGX_GetStr的参数都是a1中的某个成员,即FCGX_ParamArray和FCGX_Stream这两个结构体指针是另外一个结构体的成员。

在源码中进行查找,找到了符合的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct FCGX_Request {
int requestId; /* valid if isBeginProcessed */
int role;
FCGX_Stream *in;
FCGX_Stream *out;
FCGX_Stream *err;
char **envp;

/* Don't use anything below here */

struct Params *paramsPtr;
int ipcFd; /* < 0 means no connection */
int isBeginProcessed; /* FCGI_BEGIN_REQUEST seen */
int keepConnection; /* don't close ipcFd at end of request */
int appStatus;
int nWriters; /* number of open writers (0..2) */
int flags;
int listen_sock;
} FCGX_Request;

将a1重新设置类型,如下图所示

image-20230811124016748

FCGX_GetParam和FCGX_GetStr的参数都已发生了变化

再对其他fcgi的函数进行一些参数修改即可。

再回头看main

image-20230811125436995

修改之后的for循环发生了一点变化,还是看到sub_19600函数

先看到sub_2E428函数

image-20230811125913848

image-20230811125644886

获取CONTENT_LENGTH的值,然后再获取contentlength长度的content

将函数的变量和函数名重命名一下

image-20230811125823240

最后返回的是存储着content的数据的堆块的指针。

再看到sub_1BA4C函数

image-20230811130322885

image-20230811130414701

出现了另一个函数protobuf_c_message_unpack

这个函数属于一个反序列化框架protobuf

Protobuf (Protocol Buffers) 是谷歌开发的一款无关平台,无关语言,可扩展,轻量级高效的序列化结构的数据格式,用于将自定义数据结构序列化成字节流,和将字节流反序列化为数据结构。

protobuf支持许多语言,其中就包含c语言。protobuf-c也是开源的,既然是开源的,我们也还是扒拉下源码然后编译,然后导入结构体,以便于我们逆向分析。

安装protobuf-c之前需要先安装protobuf

1
2
3
4
5
6
7
8
9
sudo apt-get install autoconf automake libtool curl make g++ unzip -y
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.11/protobuf-all-21.11.zip
unzip protobuf-all-21.11.zip
cd protobuf-21.11
./autogen.sh
./configure
make
sudo make install
sudo ldconfig

然后继续安装protobuf-c

1
2
3
git clone https://github.com/protobuf-c/protobuf-c.git
cd protobuf-c
./configure && make && make install

安装成功之后有如下几个目录

image-20230811141411871

在lib目录下

image-20230811141449986

libprotobuf-c.so.1.0.0丢入IDA,导出结构体。

注意,导入结构体后需要修改一下size_t的大小为__int32

1
typedef unsigned __int32 size_t;

protobuf_c_message_unpack的函数原型如下

1
2
3
4
5
protobuf_c_message_unpack(
const ProtobufCMessageDescriptor *descriptor,
ProtobufCAllocator *allocator,
size_t len,
const uint8_t *data);

然后将IDA中的函数的变量重新设置

image-20230811151335218

不光protobuf_c_message_unpack,还有其他protobuf-c的函数也一并重新设置变量类型

image-20230811145941211

看到sub_21328函数

image-20230811151501754

看起来是对json数据进行解析的函数

跟入到sub_212C8函数

image-20230811151733045

发现了json_object_iter_key等函数,还是Google一下

image-20230811151832679

发现是这个开源库,依然是老样子,源码编译恢复符号表

1
2
3
4
5
6
git clone https://github.com/akheron/jansson.git
cd jansson
autoreconf -i
./configure
make
make install

恢复后如下所示

image-20230811153721454

经过上面的重命名之后,再看回到sub_19600函数

image-20230811155748167

关注一下这个descriptor,其类型是ProtobufCMessageDescriptor

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
struct ProtobufCMessageDescriptor {
/** Magic value checked to ensure that the API is used correctly. */
uint32_t magic;

/** The qualified name (e.g., "namespace.Type"). */
const char *name;
/** The unqualified name as given in the .proto file (e.g., "Type"). */
const char *short_name;
/** Identifier used in generated C code. */
const char *c_name;
/** The dot-separated namespace. */
const char *package_name;

/**
* Size in bytes of the C structure representing an instance of this
* type of message.
*/
size_t sizeof_message;

/** Number of elements in `fields`. */
unsigned n_fields;
/** Field descriptors, sorted by tag number. */
const ProtobufCFieldDescriptor *fields;
/** Used for looking up fields by name. */
const unsigned *fields_sorted_by_name;

/** Number of elements in `field_ranges`. */
unsigned n_field_ranges;
/** Used for looking up fields by id. */
const ProtobufCIntRange *field_ranges;

/** Message initialisation function. */
ProtobufCMessageInit message_init;

/** Reserved for future use. */
void *reserved1;
/** Reserved for future use. */
void *reserved2;
/** Reserved for future use. */
void *reserved3;
};

image-20230811160103536

这个结构体用于描述和表示一个Protocol Buffers消息类型。

根据这个结构体,我们可以得出很多信息

image-20230814144717091

这四个数据分别对应着magic,name,short_name和c_name。

image-20230814144855125

而下面的这些数据,挑一点重要的来讲,0x30表示根据这个protobuf消息类型实例化的c语言结构体的大小为0x30字节。

7表示fields数组的数量,fields数组是标签号排序的字段描述符数组,用于说明结构体中的各个字段的类型大小等等,fields数组的类型是

ProtobufCFieldDescriptor *,我们看到fields数组:

image-20230814145519381

ProtobufCFieldDescriptor的定义如下

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
43
44
45
46
47
48
49
50
51
52
53
54
55
struct ProtobufCFieldDescriptor {
/** Name of the field as given in the .proto file. */
const char *name;

/** Tag value of the field as given in the .proto file. */
uint32_t id;

/** Whether the field is `REQUIRED`, `OPTIONAL`, or `REPEATED`. */
ProtobufCLabel label;

/** The type of the field. */
ProtobufCType type;

/**
* The offset in bytes of the message's C structure's quantifier field
* (the `has_MEMBER` field for optional members or the `n_MEMBER` field
* for repeated members or the case enum for oneofs).
*/
unsigned quantifier_offset;

/**
* The offset in bytes into the message's C structure for the member
* itself.
*/
unsigned offset;

/**
* A type-specific descriptor.
*
* If `type` is `PROTOBUF_C_TYPE_ENUM`, then `descriptor` points to the
* corresponding `ProtobufCEnumDescriptor`.
*
* If `type` is `PROTOBUF_C_TYPE_MESSAGE`, then `descriptor` points to
* the corresponding `ProtobufCMessageDescriptor`.
*
* Otherwise this field is NULL.
*/
const void *descriptor; /* for MESSAGE and ENUM types */

/** The default value for this field, if defined. May be NULL. */
const void *default_value;

/**
* A flag word. Zero or more of the bits defined in the
* `ProtobufCFieldFlag` enum may be set.
*/
uint32_t flags;

/** Reserved for future use. */
unsigned reserved_flags;
/** Reserved for future use. */
void *reserved2;
/** Reserved for future use. */
void *reserved3;
};

image-20230814150356075

开头的version,username,token等等字符串就是发包时需要的参数,这一点在我们抓取到的网络包中可以看出来

image-20230814150504434

PROTOBUF_C_LABEL_NONE是在protobuf-c库中定义的一个枚举常量,用于表示Protocol Buffers消息字段的标签类型。

在protobuf-c库中,字段标签类型可以是以下之一:

  • PROTOBUF_C_LABEL_REQUIRED:表示字段是必需的,每个消息实例都必须包含该字段。
  • PROTOBUF_C_LABEL_OPTIONAL:表示字段是可选的,每个消息实例可以选择包含该字段。
  • PROTOBUF_C_LABEL_REPEATED:表示字段是可重复的,每个消息实例可以包含多个该字段的值。

PROTOBUF_C_LABEL_NONE则表示字段没有标签类型,即该字段无效或未设置标签类型。

可以看到,在七个数组元素中

image-20230814150700887

只有params这个元素是可以重复的,那么这个重复是怎么体现出来的,还是看到我们上面抓的包

image-20230814150805176

也就是字符串数组的形式。

注意到,在cmd这个元素中,有一个地址引起了我们的关注

image-20230814153258180

这里对应的是ProtobufCFieldDescriptor中的descriptor成员

1
2
3
4
5
6
7
8
9
10
11
12
/**
* A type-specific descriptor.
*
* If `type` is `PROTOBUF_C_TYPE_ENUM`, then `descriptor` points to the
* corresponding `ProtobufCEnumDescriptor`.
*
* If `type` is `PROTOBUF_C_TYPE_MESSAGE`, then `descriptor` points to
* the corresponding `ProtobufCMessageDescriptor`.
*
* Otherwise this field is NULL.
*/
const void *descriptor; /* for MESSAGE and ENUM types */

如果类型是PROTOBUF_C_TYPE_ENUM,那么descriptor就是指向ProtobufCEnumDescriptor类型的指针变量。

image-20230814153810656

我们将这一段设置为ProtobufCEnumDescriptor类型,

image-20230814155244975

ProtobufCEnumDescriptor定义如下

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
struct ProtobufCEnumDescriptor {
/** Magic value checked to ensure that the API is used correctly. */
uint32_t magic;

/** The qualified name (e.g., "namespace.Type"). */
const char *name;
/** The unqualified name as given in the .proto file (e.g., "Type"). */
const char *short_name;
/** Identifier used in generated C code. */
const char *c_name;
/** The dot-separated namespace. */
const char *package_name;

/** Number elements in `values`. */
unsigned n_values;
/** Array of distinct values, sorted by numeric value. */
const ProtobufCEnumValue *values;

/** Number of elements in `values_by_name`. */
unsigned n_value_names;
/** Array of named values, including aliases, sorted by name. */
const ProtobufCEnumValueIndex *values_by_name;

/** Number of elements in `value_ranges`. */
unsigned n_value_ranges;
/** Value ranges, for faster lookups by numeric value. */
const ProtobufCIntRange *value_ranges;

/** Reserved for future use. */
void *reserved1;
/** Reserved for future use. */
void *reserved2;
/** Reserved for future use. */
void *reserved3;
/** Reserved for future use. */
void *reserved4;
};

image-20230814155302130

其中的values就是cmd这个字段的可选值,这个values的类型为ProtobufCEnumValue

image-20230814155357530

cmd的可选值一共有27种,在其中我们注意到两个敏感的词

image-20230814155738949

system和popen作为两个危险函数需要格外关注,而在这里出现在了cmd的可选值中,是不是意味着有对应的函数能够执行类似system和popen的功能?

那么,这些个功能在哪里执行,对应的函数又是怎么样的?继续看回到sub_19600函数

image-20230814214523177

在这里有一个函数表的调用

image-20230814214807135

正好是27个函数,我们可以推断,这里的27个函数对应着cmd可以执行的27个功能

image-20230814214912946

而popen和system功能正好是倒数第2个第3个功能,我们在函数表中查看一下倒数第二和倒数第三个函数

image-20230814215120581

如我们所见,倒数第二个函数确实使用了popen来执行命令,这个命令来自于哪里?

image-20230814215512325

偏移0x20处,要知道,传入到函数中的参数是一个结构体,在上面已经讲过了

image-20230814215633480

而param位于这个结构体的偏移为0x20处,所以传入popen的参数实际上就是param的值,如果我们控制了param的值就能够使用popen来进行任意命令执行。

这是第一个漏洞。

有了命令执行,如果不能够未授权执行那依然相当鸡肋。

我们可以看到,在整个执行过程中并没有任何校验的代码,哪怕结构体中有usernametoken,但其类型也是PROTOBUF_C_LABEL_NONE,即传参过程中可有可无,所以即使我们只发送一个param过去也依然能够被程序正常处理,这样就能够导致未授权的RCE。

shell如下

image-20240109205119969

--- 本文结束 The End ---