Associated Vulnerability
Title:TP-LINK Tapo C200 remote code execution vulnerability (CVE-2021-4045)Description:TP-Link Tapo C200 IP camera, on its 1.1.15 firmware version and below, is affected by an unauthenticated RCE vulnerability, present in the uhttpd binary running by default as root. The exploitation of this vulnerability allows an attacker to take full control of the camera.
Description
Exploit for command injection vulnerability found in uhttpd binary from TP-Link Tapo c200 IP camera
Readme
# CVE-2021-4045
CVE-2021-4045 is a Command Injection vulnerability that allows Remote Code Execution in the TP-Link Tapo c200 IP camera. It affects all firmware versions prior to 1.1.16 Build 211209 Rel. 37726N due to insufficient checks on user input in `uhttpd`, which is one of the main binaries of the device. Full write up can be found [here](https://hacefresko.github.io/posts/tp-link-tapo-c200-unauthenticated-rce).
## Exploit ([pwnTapo.py](pwntapo.py))
This is an extended version of the PoC made to show the vulnerability at the [CryptoParty Madrid 2022](https://cryptoparty.ucm.es/). It has 2 modes:
* **shell**: Spawns a root shell on the target

* **rtsp**: Gives access to the live video stream over RTSP

## Binary analysis
Function `exec_and_read_json` uses `popen` to execute commands:

`exec_and_read_json` is used by 2 unnamed functions, which I named `set_language` and `wifi_connect`. They respectively deal with language and wifi configuration (obviously). `wifi_connect` seems to parse single quotes (`'`), however, `set_language` doesn't. This means that if we can control the input for `set_language` function, we can successfully inject our own commands.


Function `set_language` is used by `uh_slp_proto_request`, the function I mentioned before, which passes as input some parsed data received from the user.


To parse the user data, `uh_slp_proto_request` checks if it is a valid JSON object. Then, it gets a string value identified by key `"method"` and a dictionary value identified by `"params"` (at least that is what I think, since function call could not be resolved by Ghidra but seemed to work this way). Depending on the selected method, `uh_slp_proto_request` selects the function which will be called.
So, by sending `{"method": "setLanguage", "params":{}}` we successfully call `set_language` function and pass `{}` as `language_json` parameter. Then, inside `set_language`, `language_json` object is converted to string and inserted directly into `"ubus call system_state_audio set_language \'%s\'"` to be executed.
By submitting `{"method": "setLanguage", "params": {"payload": "'; touch poc;'"}}`, `ubus call system_state_audio set_language '{"payload": "'; touch poc;'"}'` will be executed, which actually contains 3 commands: `ubus call system_state_audio set_language '{"payload": "'`, `touch poc`, and `'"}'`. The second one gives us full code execution.
Now, `uh_slp_proto_request` is used by another unnamed function managing all requests, which I named `main_server_function`. If a request is valid (does not exceed maximum length, uses http or https depending on the server config, etc.), `main_server_function` checks if URL contains `/cgi-bin/luci` or `/web-static`. If it doesn't, `uh_slp_proto_request` is called.

By guessing and sending a couple of requests to the camera, we can check that data used by `uh_slp_proto_request` is regular POST data. So, if we send a POST request to `/` with the previous payload, `uh_slp_proto_request` will process this data, call `set_language` and our payload will be injected in the command executed by `exec_and_get_result`.
As you can see, I didn't mention anything about authentication, since `setLanguage` method can be called without login. This allows any user to take full control of the camera with just one unanthenticated request.
File Snapshot
[4.0K] /data/pocs/d5e1ab3e410be036a5958f12179a7bbb9bb08d55
├── [4.0K] images
│ ├── [137K] exec_and_get_json.png
│ ├── [248K] main_func_1.png
│ ├── [163K] main_func_2.png
│ ├── [241K] poc.png
│ ├── [1.0M] pwnTapo_rtsp.png
│ ├── [122K] pwnTapo_shell.png
│ ├── [201K] set_language.png
│ ├── [220K] uh_slp_proto_request_entrypoint.png
│ └── [234K] wifi_connect.png
├── [2.1K] pwntapo.py
└── [3.6K] README.md
1 directory, 11 files
Remarks
1. It is advised to access via the original source first.
2. Local POC snapshots are reserved for subscribers — if the original source is unavailable, the local mirror is part of the paid plan.
3. Mirroring, verifying, and maintaining this POC archive takes ongoing effort, so local snapshots are a paid feature. Your subscription keeps the archive online — thank you for the support. View subscription plans →