Note: This blogpost was written in November 2023, but I was waiting for the TP Link Security Team to release a fix so now it’s published(Jan 2024).
Hello world! and happy new year. It’s been a long time since I last posted here. I decided to take a new challenge, to do something I wanted to do since I was 15 years old(!) enthusiastic kid watching this Black Hat talk: hacking a Security Camera. 10 years later, I think it’s my turn now hehe
In this blogpost, I’ll share my journey of targeting the TP-Link Tapo C100 Home Security Camera. From extracting the firmware to spotting an n-day and writing a full RCE exploit.
Extracting the firmware
To get an initial foothold on the device, I soldered some cables to the UART pins of the device in hopes that I will get a bash shell.
My plan was to try a known technique used in other models of this camera: inserting an SD Card to the camera → copy /dev/mtdblock*
files to the card → plug it to my laptop → run binwalk on it.
However, for some reason the camera did not manage to detect the SD Card ;_; so what I did was:
- Dumping the whole contents of the
/dev/mtdblock*
files withxxd
(or,hexdump
) - Save all the UART output to a txt file
- Decode it back from hexdump to raw bytes
Yes, I dumped the whole firmware via UART, and it was so slow :’) But desperate times call for desperate measures.
Intro to the “dsd” binary
The dsd binary, located at /usr/bin/dsd
is one of the main components of the REST API the camera is exposing to the client.
Basically, the uhttpd
binary is using a local unix socket to send the user input to the dsd
binary, perform the necessary action(change the camera settings, etc.) and return a response.
Spotting the bug
The bug exists in the check_user_info
request handler.
The request:
The handler:
At [1]
, the RSA key is fetched and stored in pcVar3
. Later, the user input is being decrypted at [2]
.
After decrypting the user input, the function uses sscanf
to split the plaintext into two variables seperated with a :
character(i.e: AAAA:BBBB
).
The bug lays in the fact that private_decrypt
(in libdecrypter.so
) can decrypt up to 0x80 bytes:
This can trigger a buffer overflow since the buffer size in libdecrypter.so
can hold up to 128 bytes, but the stack buffer in the dsd
binary can hold much less than that(we only need 60 bytes from the beginning of the buffer to reach the return address)
After doing some more googling of strings/constants I saw in the binary, I discovered that this bug was found in another model back in 2020: TL-IPC43AN-4(discovered by CataLpa) and was not fixed in my camera model(C100). His camera was a bit different: he had Web UI(we have access only Mobile App/API) and his camera was running ARM binaries(ours is MIPS) but it looks like these cameras were sharing the same dsd
component/daemon. Moreover, I couldn’t find any other documentation of this bug(or exploit) so I guessed it might be one of those useless crashes that are not exploitable.
I decided to give it a shot anyway, with hope that maybe I’ll be the one who’ll write a full exploit for it.
So without further ado, let’s trigger the bug and examine the crash.
Triggering the bug
To trigger the bug, the following sequence of POST requests needs to sent to /stok=<YOUR_SID>/ds
:
Request 1: Get the encryption key:
Request #2: Encrypt the following payload with the key from the previous step:
QQQ:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCC
Ciphertext:
pcV7TYekRREp49SYKlCbx2NU1+3A+y8y4a2VL4hPCvqZXATsU7DicFsauJWLEw/OB0uGe2ZcHrCzXTqhk0JoDXY6Rfv/IbWeOtqOMQkDh4e0VWCk0rEAo63KuaSdnRAneWOR5j1c0ig54gFoBblJ4kHz4a4OphX6kUJce0aDQRk=
Request #3: Send the encrypted result in the following manner:
Result:
Continuing.
[dsd] check_user_info(1293): encrypt_type:2.
[dsd] check_user_info(1299): plaintext:QQQ:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCC.
[dsd] check_user_info(1303): hashPswd(QQQ) rsa_nonce(AAAAAAAAAAAAAAAA).
Thread 2 "dsd" received signal SIGBUS, Bus error.
[Switching to LWP 825]
0x42424242 in ?? ()
Nice :D
Exploiting the bug
Triggering a bug is one thing, exploiting it…well, that’s a whole engineering adventure.
Considering that fact that I have no experience with MIPS I found this very intimidating, but I’m always ready for a new challenge.
Exploiting this overflow can be trickey, because even though we can corrupt memory - we can’t enter a nullbyte(the %[^:]
format specifier in the call to scanf()
will stop after a nullbyte). So we can’t enter more than one address in order to craft a ROP/JOP chain on the stack. It requires us to find the perfect gadget: a gadget that will magically jump to system AND place an arbitrary string into the first argument.
After doing some more analysis, I found out that this primitive is ridiculously powerful and does not require any ROP/JOP chain. Because not only we control the ra
register(which allows us to take control over the program’s execution) - THE VALUE OF THE a0
REGISTER POINTS TO A STRING FROM OUR HTTP REQUEST lol(the username
field). So in other words: we don’t need to find the perfect gadget, it’s already there in our crash.
0x42424242 in ?? ()
(gdb) i r
zero at v0 v1 a0 a1 a2 a3
R0 00000000 10001c00 ffff622f 00000000 004ab739 00432685 00000000 7796bdb0
t0 t1 t2 t3 t4 t5 t6 t7
R8 00000000 00000000 31454630 32463132 39464233 46344443 46334442 00450000
s0 s1 s2 s3 s4 s5 s6 s7
R16 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141
t8 t9 k0 k1 gp sp s8 ra
R24 0044c930 779bb600 00000001 00000000 77bfd4c0 7796bef0 41414141 42424242
status lo hi badvaddr cause pc
00001c13 0d713e0e 00000008 42424242 40808010 42424242
fcsr fir restart
001c0004 00b70000 00000000
(gdb) x/s $a0
0x4ab739: "elloWorld"
To exploit this bug all you need to do is craft the following request:
We don’t need another vulnerability to break ASLR because the binary was compiled without PIE, so all we need is jump straight to system@plt
.
The full exploit is below, tested on firmware version 1.3.7:
https://twitter.com/0x_shaq/status/1723384686569836640
Thanks for reading, I hope you enjoyed :^)