Multiple Vulnerabilities in Wavlink Router leads to Unauthenticated RCE – CVE-2020-10971 and CVE-2020-10972

With everyday household items becoming “smart” and connected to the internet, I was interested in seeing how much effort companies were putting into security. I decided it would be a great hobby to buy cheap Chinese technology off of Amazon and see what I could find out.

After searching for routers, I found one from a company called Wavlink that was only $30 off of Amazon. The Wavlink WL-WN530HG4 is an Amazon’s Choice Router with “WPA/WPA2 PSK Mixed security and industry level password encryption”. Additionally, they say “We are fully confident in the design and durability of our products. If you have any issues, please do not hesitate to contact us anytime, night or day : -) “. While the WiFi password might have “industry level password encryption” it’s like putting a very secure padlock on a two foot high fence and the key is under the pot right next to it. But first, lets talk about how I came to that conclusion.

Initially setting up the router was one of the most difficult parts of achieving RCE. Maybe it has to do with my convoluted internet setup in my room at home (Modem -> WiFi Router Downstairs -> Powerline Ethernet Adapter -> Switch in my room -> Wavlink Router) but it kept redirecting me to a page that was half in Chinese telling me I wasn’t connecting directly to the router, even though I was going straight to its IP address. Either way, I did eventually get it set up and started exploring.

I was running Burp while I was doing the initial setup, and one of the things I noticed right away was that after I logged in and was viewing the home page, it looked like calls were being automatically made to a few endpoints to update stats.

Automatic requests while logged into the router

The endpoints all followed the format “live_(string).shtml” and had what appeared to be some kind of session id called “r” that was being sent with the request. Just for fun, I tried going directly to one of the pages without the “r” parameter and I was still able to view the response. Even after I logged out and could not get to the main page, the “live_(string).shtml” endpoints were all still reachable – it appeared no authentication or session ID was required to get there.

live_speed.shtml returns incormation without potential ID as parameter

Now that I had a basic feel for how the router worked, I decided to take it apart. Serial pins is something that has interested me ever since I first heard about them. The idea of a free backdoor is just too juicey for me to pass up. Plus, finding a way to get into a device remotely is much easier when you start from the inside.

Turns out, trying to take the router apart was the second hardest part about achieving RCE – there was a lot of little plastic clips holding the top on, and I missed two screws hidden under a label which was preventing me from making any significant progress. After 15 minutes of prying I realized what I had missed, and I finally had my first look at the board.

The router before I tried to demolish it
It took me way to long to find these bad boys
Finally got the lid off. Immediately noticed the groups of pins at the top

I had my multi-meter and jtagulator at the ready, but fortunately Wavlink was kind enough to label the serial pins for me.

TX, RX, GND, and VCC identified in 2 seconds flat

I soldered on a couple of wires, and using my Attify badge and this handy baudrate script, I was able to establish a serial connection with minicom.

RX TX and GND connected to the Attify Badge and then to my PC
Using the baudrate script – activity seen right away, cleartext identifies baudrate as 57600

Minicom session gets a prompt

At the end of the bootup I was presented with a “WAVLINK login:” prompt. I tried using several normal usernames like root or admin with the password I set in the gui, but nothing worked. I was a bit dissapointed, but after I looked back through the boot logs I found this line about a password change that always appeared:

First mention of this user – no username is entered in the GUI

I decided to try logging in with “admin2860” and the gui password and boom – I was in. It appeared to have a version of BusyBox running on the router. The serial connection was a success, but super annoying to use because whenever you try and type or view a file it decides to spit out a log for some activity happening on the router. Fortunately, it has a built in telnet binary. After starting it, i was able to connect on a port of my choosing with admin2860 and not worry about logs screwing up my view.

Successful login via serial connection

Now that I had a solid connection, I started to explore what webpages were available to the end user. The web directory appeared to be /etc_ro/lighttpd/www/ and listed way more webpages then I had uncovered in my manual exploration. Besides the first few endpoints that were automatically hit, I found tons of “live_(string).shtml” pages that did not require any sort of authentication to get to. And this is where the fun begins.

One of the first pages I checked in the GUI had the below interface. It appeared to be a way to pause some functionality for testing purposes – and of course, no authentication is required to get to it.

It did have an input for your password, so I thought that maybe there was some security on this thing after all. I tried to compare the authentication method with the main login page and boy was I surprised by what I found.

Basic string comparison for the password – admpass is the input text box

Instead of sending my password off to be verified on the back end, it appeared to be checked against a local variable. Curious as to how this was done, I did a quick search for “password” and I literally laughed out loud as to what I found.

Input password compared against plaintext variable

Low and behold, there was my super secret password in plain text, with the admin username in plain text, on a page that requires no authentication of any kind to view. Considering how they already have a built out authentication process used for other pages, its very curious that a developer would go out of their way to set up something different for this one page in such a blatantly insecure manner. Thanks to my handy backdoor, I was able to find that a command is executed on the backend to populate that syspasswd variable every time this page is loaded – that way in case you change the password, you don’t have to worry about this page being out of date.

Running the command myself returns the current password, in plaintext

So what does that get us from the perspective of a remote attacker? We have the ability to get the current admin credentials, and we can get a shell if the telnet binary is started. However, most remote attackers wont be able to solder on any wires, so I wasn’t going to stop there.

Going through the rest of the pages in the www directory, there is another web page that provides this interface:

A built in GUI for command execution as root – could it be that easy?

Obviously this immediately grabbed my attention. I tried a simple “ls” command and the page redirected me to /cgi-bin/(null) and errored out. This was a disappointing result, but when I copy-pasted the address back in to relook at it, I found that the command had appeared to actually execute and it returned the results.

The output of my sample ls command

It appears that the command is executed as the admin2860 user in the /etc_ro/lighttpd/www/cgi-bin directory, and although the redirect feature is broken the command is still executed. I was pleasantly surprised that this page was not accessible without being logged in. Of course, since the admin credentials are already exposed this doesn’t buy you much security – by just going to one other page, you can find the info to log in and execute commands.

Additionally, I stumbled across an interesting fact. While you cannot access the web page for the Command Execution interface, you can still make a POST request to adm.cgi. If it’s in the proper format, and a user is logged in, then the command is executed. While the router appears to do some verification to ensure that someone is authenticated at that time, it makes no discernible effort to verify that the POST request actually came from the user that is authenticated. So even if the endpoint with the plaintext credentials is removed, an attacker could just make a loop of post requests to adm.cgi and as soon as a user logged in they would achieve command execution.

In conclusion, a remote attacker can achieve RCE via a POST request to adm.cgi. There are several conditions required, including proper parameters and an active session. However, these conditions can all be met without any initial authentication required thanks to several specific exposed “live_(string).shtml” endpoints – so an attacker with the right background information about the device could achieve RCE fairly easily.

I did not go very in depth about maintaining persistence on the device outside of the initial exploit chain. I tried creating additional users but they appear to be removed on boot – I believe I could circumvent this with enough time, but due to the already exposed flaws I didn’t feel it was worthwhile spending the time on it.

I attempted multiple times to contact Wavlink about the issues I found via several different support contacts on their site but I was never able to get a response from them. After weeks of no communication, I submitted a report to Mitre and CVE-2020-10971 and CVE-2020-10972 were created, covering this device.


2/19/2020 – Initial discovery and notification
2/29/2020 – Follow up notification after no response
3/23/2020 – Follow up notification after no response indicating plans to submit for CVE
4/3/2020 – Additional communication attempts failed to get response, CVE request submitted
5/7/2020 – CVE published
6/30/2020 – Additional details published

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s