Newly Unreachable: The very sad story of a TP-Link bug

by

Max VA (@maxpl0it)

August

2023

The Introduction

Boy, as a company do we love Pwn2Own. As a company we schedule a yearly event that to go on a trip (Last year was glamping) and hack away for a week for fun. At the time of the contest, the Interrupt Labs team had a number of bugs, a number of exploits (including a few backup exploits), and were ready to pop some devices! Even the folks from our Vulnerability Researcher Development Program (VRDP) were getting involved and doing some really great stuff.

However, not all bugs are meant to be. At one point we were lacking a TP-Link WAN bug, so I went to work having a gander at my old friend. I took a binary I had looked at many times, just to see if there was anything I had missed.

The Background

Back in 2019 I was new to working with actual security researchers and had just been hired by a company called MWR Labs/F-Secure (Many of whome are at Interrupt now - Somewhat poetic that it's come full circle). I'd managed to work my way into getting involved in the Pwn2Own Tokyo efforts and had a router to pop. In the firmware there were some interesting binaries called tdpServer and tmpServer.

tdpServer was listening on the LAN IP, and tmpServer was purely listening on localhost (weird, right?). As it turned out, there was also an SSH server running on the device that existed just for you to SSH forward the tmpServer port (And no, the SSH server has shell connections disabled, so forward was as good as it got). These two binaries were used for the TP-Link OneMesh system, which allows extenders and other devices to communicate with the router in an efficient and proprietary format.

Both tdpServer and tmpServer had command injection bugs that I used in the contest for a LAN and a WAN exploit.

In order to turn get the tmpServer accessible from the WAN, I used the fact that IPv6 WAN filtering was broken on these devices, which allowed me to access the SSH server.

Queue flashy lights sequence


The Bug

With the command injection bugs long fixed, I went back down the path of "let's have a look around". While I was scrolling and re-renaming things on the latest tmpServer binary, I rediscovered something I'd noticed years ago: A function that flat out didn't decompile decently. In 2019 I'd just glossed over it as probably a decompiler error, but in 2023 I was smarter and had an idea what was happening (Personal growth, I guess?).

The function in question is used for logging messages:




And decompiled as so:



Weird, right? Exploring the assembly, you notice that it's adding some space onto the stack pointer as so:



Which will give us the following stack layout when vsprintf is called:



This makes it a little clearer what the function is doing - Instead of allocating space on the heap for the output buffer for vsprintf, it simply adds some space to the stack which can be used as a temporary buffer before it's used with the fprintf below it. This way there's no need for allocating heap space which will only have to be deallocated after fprintf is called.

However there's a fatal flaw in this logic. The size calculations (the two strlen calls) only take into account the size of the "filename and line number" string and the actual format string itself when checking if the result would fit in this stack space. It doesn't take into account the size of the arguments to the format string, which could be of variable number and each of variable size. Therefore, if there is a place in the code which calls this logging function and uses user-controlled data, this could cause a classic stack overflow:




The Trigger

So we have a bug, now we need to find a vulnerable path. Looking at the xrefs to the logging function, there are around 500.

Yes, I manually went through each and every one to find the best option for this bug (Dedication?).

I landed on a function that parses JSON supplied by the attacker in a request, and then outputs the value of the "name" key in it:





In order to actually reach this, you need several packets to set up the state of the tmpServer client object and select the function pointer we're targeting, extra null bytes at the end, and a CRC32 checksum. You can find the structures and packets documented from our old post here.

Once you've got all those in place, you can reach the vulnerable line with the following JSON:


{"owner_id":1,"name":"PAYLOAD_HERE","internet_blocked":false}


The payload in "name" needs to contain the following:

  1. 9069 bytes of padding ("A"s will suffice)
  2. Any readable address (You're overwriting a pointer here that will be dereferenced before the termination of the function)
  3. 4 bytes of padding
  4. Another readable address (Same as above)
  5. 32 Bytes of padding
  6. A the value for the link register address (which will end up in $pc)



The Pain

This was all very exciting! Now, how do we reach tmpServer in modern TP-Link devices?

Well it turns out that TP-Link had finally caught onto people using this IPv6 WAN bypass and patched it. That means there is no longer any way to access tmpServer on the WAN. Even on the LAN the issue is that you would need the admin password to set up the SSH tunnel...

Our very nice little bug just got boring.


The End

Frustrated by the fact this bug was no longer reachable in any TP-Link devices, I ended up reporting this stupid bug directly to TP-Link. To my surprise, they patched it pretty quickly (~2 months from report to fix over the Christmas period, including the questions they came back to me with). I guess things are improving in at least one way!

Please click on "Preferences" to confirm your cookie preferences. By default, the essential cookies are always activated. View our Cookie Policy for more information.