Tapo C310 is a full-featured weatherproof security camera that you can access from anywhere. Receive instant notifications and check feeds when the motion is detected. Moreover, the automatic siren system will trigger light and sound to frighten away unwanted visitors after the camera detects motion. Day or night, rain or shine, the Tapo camera protects what you love most.source
My “investigation” begins with a TP-Link Tapo C310 IP camera, which is a type of smart camera connected to the Internet and reachable through any web browser and an IP address. From the documents provided by the manufacturer, no additional information was given about the type of hardware architecture (CPU and RAM) or the security of the device. Only the software can reveal clues about what is actually inside the hardware, so all that remains is to analyse the camera’s firmware.
Music provided by COMA - A-Train, press Play and let’s proceed.
Smartphone app
TP-Link provides the Tapo smartphone app (iOS, Android) but the application itself is not functional without a TP-Link Cloud account. After creating the acount you will be able to set up your camera inside the app and you can access it from anywhere provided you have an Internet connection.
Setting up the camera is a matter of connecting your smartphone to the camera’s WiFi access point (in the form of Tapo_Cam_XXXX
, where XXXX are the last four digits of the device’s MAC address) and following the instructions in the Tapo application. The communication between the smartphone and the camera is encrypted, however, the access point is open, meaning there is a timeframe in which the device could be compromised during setup.
The Tapo application requires Internet acccess and perfoms the following requests each time it’s open:
n-wap-gw.tplinkcloud.com
onTCP
port443
(regional)iac.tplinknbu.com
onTCP
port443
n-oauth.tplinkcloud.com
onTCP
port443
n-aps1-wap-gw.tplinkcloud.com
onTCP
port443
(regional)api-in-app-marketing-auth.i.tplinknbu.com
onTCP
port443
aps1-app-server.iot.i.tplinknbu.com
onTCP
port443
(regional)download.tplinkcloud.com
onTCP
port80
aps1-app-tapo-care.i.tplinknbu.com
onTCP
port443
(regional)sg-stun.tplinkcloud.com
onUDP
port3478
(regional)aps1-security.iot.i.tplinknbu.com
onTCP
port443
(regional)aps1-cipc-api.i.tplinknbu.com
onTCP
port443
(regional)analytics.tplinkcloud.com
onTCP
port443
As you can see, the update checks are done over HTTP and not HTTPS but the firmware is signed anyway so I don’t see a problem here. The regional requests are done to specific servers depending on your current location (aps1
will become euw1
if you’re in Europe).
Video stream
Tip
In firmware version 1.3.0 and below, if you did not set-up a Device Account the default password was TPL075526460603
(as seen from the /bin/cet
binary) and anyone on your local network could get unauthorized access to the camera’s RTSP video stream, so it’s not as if TP-Link’s security is stellar.
If you want to view the video stream inside a media player like VLC, you will need to enable Device Account inside the Tapo smartphone app and choose a username and password. After doing that you can open the stream URL inside VLC.
$ vlc rtsp://USERNAME:PASSWORD@CAMERA_IP/stream1
For example, if your Device Account username is bill
, password is clinton
and camera IP is moni… I mean 192.168.1.69
, you can open this stream:
$ vlc rtsp://bill:[email protected]/stream1
You can replace stream1
with stream2
for a low quality video stream.
$ vlc rtsp://bill:[email protected]/stream2
If you’re using the Debian-packaged vlc
, be aware of the fact that rtsp support is not enabled. So, use mpv
.
$ mpv rtsp://bill:[email protected]/stream1
Warning
Do not mistake the TP-Link Cloud username and password with the Device Account username and password. The Cloud username and password are used for logging into the Tapo smartphone app and the same password can be used to perform API requests to your camera. The Device Account username and password are needed ONLY for viewing the video stream(s) in a third-party app.
Hardware
Manufacturer | Code | Description | Datasheet |
---|---|---|---|
Realtek | RTL8192EU | Wireless LAN 802.11n USB 2.0 Network Adapter | link |
SigmaStar | SSC335 | High-Integrated IP Camera SoC Processor, ARM Cortex-A7 800MHz Single Core with 512MB DDR2 | link |
XMC | XM25QH64C | 3V 64Mb serial flash memory with dual/quad SPI & QPI | link |
JWD | S16037G | 10/100Base SMD Transformer module | link |
API
To perform API requests to your Tapo camera you will need to use the Tapo Cloud password to generate a security token.
If you want to continue I’ll asume that:
- You have a TP-Link Tapo C310 camera and you connected it to the mains power source.
- You opened the Tapo application on your smartphone, created a Tapo Cloud account and you remember the password to it.
- You added the camera to the Tapo application and you can view video stream from it, meaning all is set-up well.
We’ll start by hashing the password, remember to replace CAMERA_PASSWORD
in the line below with your Tapo Cloud password. The username (email address actually) is not required, we’ll be using the generic admin
username.
$ python3 -c 'import hashlib; password="CAMERA_PASSWORD"; hashedPassword=hashlib.md5(password.encode("utf8")).hexdigest().upper(); print(hashedPassword)'
BE9A9CBA5264F3A6DB62906AEA447CF5
Next step is to use the hashed password and the admin
username to get the stok
token that will allow us to do authenticated requests to the camera API. Obviously, replace HASHED_PASSWORD
with the string from the previous command (not the actual password, the hashed one) and the IP of your camera (mine is 192.168.1.69
).
$ wget -O- --no-check-certificate --quiet --method POST --timeout=0\
--header 'Content-Type: text/plain'\
--body-data '{ "method" : "login", "params": { "hashed": True, "password": "HASHED_PASSWORD", "username": "admin"}}'\
'https://192.168.1.69'
Beautified JSON response:
{
"error_code": 0,
"result" : {
"stok": "21a58ea29ffa45a60b76485e12e10053",
"user_group": "root"
}
}
Now that we have the stok
token, it’s time to get some basic info about the camera. Replace 192.168.1.69
with your camera IP and use your security token from the step above, in the https://192.168.1.69/stok=21a58ea29ffa45a60b76485e12e10053/ds
line:
$ wget -O- --no-check-certificate --quiet --method POST --timeout=0\
--header 'Content-Type: application/json; charset=UTF-8'\
--body-data '{'\''method'\'': '\''multipleRequest'\'', '\''params'\'': {'\''requests'\'': [{'\''method'\'': '\''getDeviceInfo'\'', '\''params'\'': {'\''device_info'\'': {'\''name'\'': ['\''basic_info'\'']}}}]}}'\
'https://192.168.1.69/stok=21a58ea29ffa45a60b76485e12e10053/ds'
Beautified JSON response:
{
"result": {
"responses": [{
"method": "getDeviceInfo",
"result": {
"device_info": {
"basic_info": {
"ffs": false,
"device_type": "SMART.IPCAMERA",
"device_model": "C310",
"device_name": "C310 1.0",
"device_info": "C310 1.0 IPC",
"hw_version": "1.0",
"sw_version": "1.3.2 Build 220901 Rel.74359n(4555)",
"device_alias": "camera1",
"avatar": "Garage",
"longitude": 0,
"latitude": 0,
"has_set_location_info": 0,
"features": "3",
"barcode": "",
"mac": "REDACTED",
"dev_id": "REDACTED",
"oem_id": "15483C2A4CF035EA06109A2782F10C2C",
"hw_desc": "00000000000000000000000000000000",
"is_cal": true
}
}
},
"error_code": 0
}]
},
"error_code": 0
}
To check the firmware update status, perform another request (don’t forget to replace the security token and the camera IP):
$ wget -O- --no-check-certificate --quiet --method POST --timeout=0\
--header 'Content-Type: application/json; charset=UTF-8'\
--body-data '{'\''method'\'': '\''multipleRequest'\'', '\''params'\'': {'\''requests'\'': [{'\''method'\'': '\''getFirmwareUpdateStatus'\'', '\''params'\'': {'\''cloud_config'\'': {'\''name'\'': '\''upgrade_status'\''}}}]}}'\
'https://192.168.1.69/stok=21a58ea29ffa45a60b76485e12e10053/ds'
Beautified JSON response:
{
"result": {
"responses": [{
"method": "getFirmwareUpdateStatus",
"result": {
"cloud_config": {
"upgrade_status": {
"state": "normal",
"lastUpgradingSuccess": true
}
}
},
"error_code": 0
}]
},
"error_code": 0
}
To get a list of functions supported by your camera (module_spec
function), replace camera IP and security token:
$ wget -O- --no-check-certificate --quiet --method POST --timeout=0\
--header 'Content-Type: application/json; charset=UTF-8'\
--body-data '{'\''method'\'': '\''get'\'', '\''function'\'': {'\''name'\'': ['\''module_spec'\'']}}'\
'https://192.168.1.69/stok=21a58ea29ffa45a60b76485e12e10053/ds'
Beautified JSON response:
{
"function": {
"module_spec": {
".name": "module_spec",
".type": "module-spec",
"app_version": "1.0.0",
"led": "1",
"change_password": "1",
"local_storage": "1",
"timing_reboot": "1",
"ota_upgrade": "1",
"msg_push": "1",
"msg_alarm": "1",
"smart_detection": "1",
"smart_msg_push_capability": "1",
"custom_area_compensation": "1",
"ae_weighting_table_resolution": "5*5",
"network": ["wifi"],
"events": ["motion", "tamper"],
"playback": ["local", "p2p", "relay"],
"preview": ["local", "p2p", "relay"],
"video_codec": ["h264"],
"audio": ["speaker", "microphone"],
"download": ["video"],
"record_type": ["timing", "motion"],
"record_max_slot_cnt": "10",
"stream_max_sessions": "10",
"streaming_support_versions": ["1.0"],
"relay_support_versions": ["1.3"],
"p2p_support_versions": ["1.1"],
"device_share": ["preview", "playback", "voice", "cloud_storage"],
"msg_alarm_list": ["sound", "light"],
"storage_api_version": "2.2",
"playback_scale": "1",
"lens_mask": "1",
"target_track": "0",
"wireless_hotspot": "1",
"wifi_connection_info": "1",
"greeter": "1.0",
"linecrossing_detection": "1",
"intrusion_detection": "1",
"audioexception_detection": "0",
"video_detection_digital_sensitivity": "1",
"client_info": "1",
"multicast": "0",
"privacy_mask_api_version": "1.0",
"ssl_cer_version": "1.0",
"backlight_coexistence": "1",
"media_encrypt": "1",
"multi_user": "0",
"auth_encrypt": "1",
"cloud_storage_version": "1.0",
"ai_enhance_capability": "1",
"wifi_cascade_connection": "1",
"custom_auto_mode_exposure_level": "0",
"verification_change_password": "1",
"linkage_capability": "1",
"msg_alarm_separate_list": ["light", "sound"],
"http_system_state_audio_support": "1"
}
},
"error_code": 0
}
To figure out all the methods, functions and parameters you will need access to the firmware. So, let’s inspect that.
Tip
The default credentials can be found out inside the /usr/bin/logrecordd
binary, user is root
and the password is slprealtek
. This will be useful if you attach a serial console to the board. Looking at online photos I was unable to spot serial pads like in older TP-Link cameras.
Firmware
The firmware itself is the type of software that communicates with and controls the hardware components of the camera; it is the first piece of code that runs on the device, it loads the operating system and provides specific services to run programs, interacting with various hardware components.
Reversing the firmware involves disassembling and understanding the inner workings of the device: this can range from simple analysis (looking at various aspects of the device, such as its file system and various interfaces) to an in-depth look at the firmware itself and uncovering the internal details and algorithms of the firmware.
Versions
- 1.3.2_Build_220901_Rel.74359n, local mirror, latest version
- 1.3.0_Build_220328_Rel.64283n, local mirror
- 1.1.20_Build_220622_Rel.68343n
- 1.1.14_Build_220317_Rel.58905n
- 1.1.12_Build_211126_Rel.15622n
- 1.1.10_Build_210825_Rel.70571n
It’s hard to find download links for the older firmware versions because the UNIX timestamp is added to the end of the filename, so unless you want to bruteforce it, this is all I have.
Prerequisites
If you’re on macOS:
$ brew install binwalk dtc squashfs
If on a Debian-flavoured Linux:
$ sudo apt install binwalk squashfs-tools device-tree-compiler
Analysis
Start by downloading the latest firmware for the Tapo C310 camera from the manufacturer’s website.
$ wget http://download.tplinkcloud.com/firmware/Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin
A quick file
on the firmware did not highlight the type of file and returned a simple data
, therefore the file
utility was unable to identify the precise type of file. Fortunately, another utility called Binwalk makes much of this work easier by analyzing and extracting the firmware.
$ binwalk Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
2048 0x800 uImage header, header size: 64 bytes, header CRC: 0xC7B58C50, created: 2022-09-01 12:28:57, image size: 60120 bytes, Data Address: 0x0, Entry Point: 0x0, data CRC: 0xBA49C792, OS: Firmware, CPU: ARM, image type: OS Kernel Image, compression type: lzma, image name: "MVX1##I6gd1dfee1CM_UBT1501###XVM"
2112 0x840 xz compressed data
133632 0x20A00 uImage header, header size: 64 bytes, header CRC: 0x84F0E212, created: 2022-09-01 12:39:16, image size: 1585816 bytes, Data Address: 0x20008000, Entry Point: 0x20008000, data CRC: 0xB38164A0, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "Linux-4.9.84"
148579 0x24463 xz compressed data
148913 0x245B1 xz compressed data
1722368 0x1A4800 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 5258362 bytes, 863 inodes, blocksize: 65536 bytes, created: 2022-09-01 12:39:23
7015680 0x6B0D00 gzip compressed data, from Unix, last modified: 2022-09-01 12:39:24
7038980 0x6B6804 gzip compressed data, from Unix, last modified: 2022-09-01 12:39:24
And you can recursively extract the firmware file using binwalk
:
$ binwalk -Me Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin
Scan Time: 2023-07-21 13:28:48
Target File: Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin
MD5 Checksum: eca38a77393bef3e522dab418a3df7b2
Signatures: 411
...
If you want to extract the flattened device tree, first you will need to carve the secondary Linux image (at offset 133632
) using dd
, use binwalk to extract the contents of the image file and then we can use dd
again to carve the FDT from a file inside the Linux image.
$ dd if=Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin of=linux_image bs=1 skip=133632 count=1585816
1585816+0 records in
1585816+0 records out
1585816 bytes transferred in 3.048178 secs (520250 bytes/sec)
$ file linux_image
linux_image: u-boot legacy uImage, Linux-4.9.84, Linux/ARM, OS Kernel Image (Not compressed), 1585816 bytes, Thu Sep 1 12:39:16 2022, Load Address: 0X20008000, Entry Point: 0X20008000, Header CRC: 0X84F0E212, Data CRC: 0XB38164A0
$ binwalk -Me linux_image
Scan Time: 2023-07-21 19:28:03
Target File: linux_image
MD5 Checksum: 6fdf778179dc62f4f4adcf75ce5677f6
Signatures: 411
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 uImage header, header size: 64 bytes, header CRC: 0x84F0E212, created: 2022-09-01 12:39:16, image size: 1585816 bytes, Data Address: 0x20008000, Entry Point: 0x20008000, data CRC: 0xB38164A0, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "Linux-4.9.84"
14947 0x3A63 xz compressed data
15281 0x3BB1 xz compressed data
Scan Time: 2023-07-21 19:28:03
Target File: _linux_image.extracted/3BB1
MD5 Checksum: 890e9497415f63dec9219d0deff5aa43
Signatures: 411
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
932152 0xE3938 SHA256 hash constants, little endian
2211176 0x21BD68 DES SP2, little endian
2211944 0x21C068 DES SP1, little endian
...
2929435 0x2CB31B LZMA compressed data, properties: 0xC0, dictionary size: 0 bytes, uncompressed size: 64 bytes
2970448 0x2D5350 Flattened device tree, size: 40469 bytes, version: 17
The FDT is at offset 2970448
in the _linux_image.extracted/3BB1
file and has a size of 40469
bytes, so let’s extract it.
$ dd if=_linux_image.extracted/3BB1 of=device_tree bs=1 skip=2970448 count=40469
40469+0 records in
40469+0 records out
40469 bytes transferred in 0.109888 secs (368275 bytes/sec)
$ file device_tree
device_tree: Device Tree Blob version 17, size=40469, boot CPU=0, string block size=1269, DT structure block size=39144
And now we can analyze the flattened device tree using a tool called device-tree-compiler
, a tool that will convert the Device Tree Blob (DTB) to a Device Tree Source (DTS):
$ dtc -I dtb -O dts -o - flattened-device-tree.dts
You can view/download the extracted .dts file here (it’s a text source file). Inside there is a summary of all hardware devices on the board but the most important part is this:
model = "INFINITY6 SSC009A-S01A QFN88";
compatible = "sstar,infinity6";
chosen {
bootargs = "console=ttyS0,115200n8r androidboot.console=ttyS0 root=/dev/mtdblock2 init=/linuxrc cma=16m";
};
If you want to browse the whole DTS file be aware of the fact that the information inside is fairly technical and you will need to be familiar with CPUs, clocks, UART devices, NAND flash memory, MMC controllers, etc.
Downgrading
Warning
Do not downgrade the firmware unless you know what you are doing. Never expose a vulnerable device (especially a security camera) to the Internet.
If you want to downgrade the firmware, you can do it in an undocumented way: there is a check_upgrade
init service that checks upon the device start-up whether the SD card partition is available and if so, checks if there is a specific file on it (factory_up_boot.bin
). If that file exists, the init service starts the firmware upgrade with this file. The check_upgrade
file can be found inside the /etc/init.d
directory and it’s shown below:
#!/bin/sh /etc/rc.common
START=13
start() {
if [ ! -d "/tmp/sdcard" ]; then
mkdir /tmp/sdcard/
fi
if [ -b /dev/mmcblk0p1 ]; then
mount -t vfat /dev/mmcblk0p1 /tmp/sdcard/
else
exit 0
fi
echo "check firmware upgrade"
if [ -e /tmp/sdcard/factory_up_boot.bin ]
then
echo "start firmware upgrade ..."
slpupgrade -n "/tmp/sdcard/factory_up_boot.bin"
while true
do
sleep 10
done
else
umount /tmp/sdcard
fi
}
slpupgrade
doesn’t compare version numbers, it just checks the file signature, checksum, camera type and revision, etc.
Others
Linux kernel
Based on OpenWrt Attitude Adjustment 12.09-rc1 running Linux 4.9.84.
root@SLP:~# cat /etc/openwrt_version
12.09-rc1
Open ports
$ nmap -sV -p- 192.168.1.69
Nmap scan report for 192.168.1.69
Host is up (0.0055s latency).
Not shown: 65531 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
443/tcp open ssl/nagios-nsca Nagios NSCA
554/tcp open rtsp
2020/tcp open soap gSOAP 2.8
8800/tcp open sunwebadmin?
Telemetry
Telemetry data is POSTed to the https://analytics.tplinkcloud.com
API endpoint via the /bin/telemetry
and /bin/cloud-service
binaries. Telemetry collection is done from within the /bin/create_telemetry_files
and /usr/sbin/collect_wifi_info
binaries.
Tip
The sane thing to do would be to block all connections to analytics.tplinkcloud.com
from a router.
Photos
Click on one of the images for a higher resolution photo.