Inside a Brute Force Router Takeover

Routers provide access to the Internet for millions of users, but hackers are also using routers to gain access into your home network. In this article I expose the dangers of default credentials by performing a brute force router takeover.

Many modern home routers offer a remote management feature, and luckily router manufacturers have stopped enabling this by default. It is not very difficult to activate this functionality. Once enabled an attacker can access and modify any settings on your router from anywhere in the world. Worst of all, your router will not give any indication that it has been compromised until it is far too late.

I decided to spend the weekend attempting to exploit this feature in my home router, a NETGEAR WNDR4300. This article will cover the analysis of the router administration pages. I then describe the development of a Python script to programmatically enable remote management. This example of a brute force router takeover will make you want to change your default router credentials if you haven’t already! In the follow up article I will cover how this same functionality may be exploited by a weaponized website.

Disclaimer

When analyzing cybersecurity it is often useful, and even critical to take the mindset of an attacker. This should not be interpreted as an endorsement for illegal cyber activities. I take no responsibility for anything you do with this information. I provide it only with the hopes that it will enhance your knowledge of cybersecurity. Hacking networks and routers that you do not own is illegal unless you have express permission from the owner to do so. Always run your experiments in a controlled test environment that you own or have permission to use. 

With that bit out of the way, let’s get started!
If you would like to download the code developed in this article and follow along head on over to Github and download it there: PORTAL on Github

Analyzing the Administration Pages

I started this brute force router takeover by pulling up my router administration page and logging in, but not before starting Wireshark to sniff HTTP traffic. The administration console all operate with plaintext HTTP. This made it very easy to sniff the packets in Wireshark.

Like most routers, my router also uses basic authentication to access the router administration pages. Default credentials are available for most routers on the Internet, and I’m willing to bet you haven’t changed yours.
This was the first piece of the puzzle I wanted to test. I decided to use the Python Requests library to test if I can authenticate against the router. Requests even has a shortcut method for passing basic authentication credentials shown below.

>>> import requests 
>>> r = requests.get('http://admin:password@192.168.1.1/index.htm') >>> r.status_code
200

Perfect! HTTP status 200 means we have authenticated successfully. With this bit working, I knew I could easily loop through a few password lists to test default router credentials.

Enable Remote Management

The next piece of the puzzle is to figure out exactly how remote management is enabled. I navigated to this section of the administration pages and pulled up the page source in my browser.

Remote management form data

My router uses a combination of HTML frames and Javascript to display the administration pages. The remote management settings are a simple form which get submitted to /apply.cgi.

<form method="POST" action="/apply.cgi?/FW_remote.htm timestamp=75866660299840" target=formframe>

The administration page that is being edited is passed as a URL parameter, in this case /FW_remote.htm. There is also an additional parameter called timestamp. If you are a curious type and plug this timestamp into a UNIX timestamp converter you may find that it corresponds to a date of February 13, 4374. That doesn’t seem right…

Refreshing the page changed the value, but not as one might expect for a timestamp. It was a completely new random number with another random date.

Form Tokens

This strangely behaving timestamp led me to believe that it wasn’t a timestamp at all. I began to suspect it was a type of form token to prevent automated attacks against the form. I copied the HTML out of the browser and recreated the form in a simple HTML file on my local machine.

Submitting the form in my browser correctly enabled remote management. If I try to submit the form again however, the request fails. This makes sense if the timestamp is in fact a token as it would have already been ‘used’ in the first request.

I went back over to the page source for the administration page and refreshed so I could grab a new token. I copied this new token into my local HTML form and tried submitting the form again. Once again I had successfully enabled remote management on the router.

In summary: The router serves up the administration page with a unique timestamp value in the form target. This same token must be present when submitting the form or the request will fail.

The timestamp parameter I discovered in this form is in fact the form token. This is a minor nuisance as we can still load the page initially and scrape the timestamp value before submitting our form.

Automating the Attack

Surely we can’t be expected to try each combination of user and password manually! This is where can leverage just a little bit of Python.
The first step is determining the correct credentials to use. Luckily these passwords are usually left as the default and are freely available online. We start by having our Python script load data from two sets of files, users.txt and passwords.txt. These values are simply stored in a Python list.

Python: load username and password files
# Empty lists of usernames and passwords 
user_names = [] 
passwords = [] 
# Load usernames 
f = open('users.txt','r') 
data = f.readlines() 
# Strip removes the newline from the end of each row 
user_names = [user.strip() for user in data] 
# Load passwords 
f = open('passwords.txt','r') 
data = f.readlines() 
# Strip removes the newline from the end of each row 
passwords = [password.strip() for password in data] 
# Print the loaded information 
print 'Loaded users: {}'.format(user_names) 
print 'Loaded passwords: {}'.format(passwords)

We then use a nested for loop to iterate through each password for each username. Each iteration through the loop we test the next user and password combination and submit the request to the router IP.

# For each username... 
for user in user_names: 
  # Test each password... 
  for password in passwords: 
    # Test username and password here

If we receive a 200 response, we can assume we figured out the credentials most of the time (more on this later). During testing I started with a single username, and only two passwords. A correct one, and an incorrect one.
The Python script tests the first, incorrect password, receives a 401 response, and then proceeds to try again with the second, correct, password. At this point the script receives a 200 response and stores the user and password combination as verified_user and verified_pass.

False Positives

It wasn’t until I began running more realistic tests with an increased combination of usernames and passwords that I noticed something odd. It seemed as though it would always succeed on the fourth attempt regardless of which password was used. I added a debug statement to the code which dumps out the received HTML content on each response.

It turns out after 3 failed login attempts my router does not respond with a 401 status code. Instead it responds with a 200 status code, and an Unauthorized Access page. My code only performed checks on the returned status code resulting in a false positive. The page that is returned simply has 401 Unauthorized in the title, so I added an additional check in for this string in the response body.

With this modification my script happily tested passwords until it actually received the correct one.

False Negatives

From time to time it also appeared that the script would never find the correct password. I assumed this was due to the speed with which I was sending requests. I added a small delay to the main loop and performance increased drastically.

Opening the Portal

Now that we are able to successfully access the router administration pages, it is time to see what we can alter. Ideally we want to open a remote management port on the Internet side of the router. This allows us access the router whenever we want with out depending on the users machine. As we saw in the initial analysis, enabling remote management is a simple matter of submitting an HTML form with the correct ‘timestamp’ token.
The first step is getting the code to load the administration page we are after, and scraping the timestamp value from the response. The code below shows the page request process and the use of a simple regex to extract the timestamp value. This piece of the code runs as it iterates through the username and password values. If the correct username and password is found, the timestamp value is extracted.

Python: brute force passwords
# Build full URL with username and password from list 
url = 'http://{}:{}@{}/{}'.format(user, password, target_ip, target_page) 
# Retrieve URL 
r = requests.get(url) 
# If we authenticated successfully, extract timestamp 
if r.status_code == 200 and '401 Authorization' not in r.text:
  print '[+] Found correct user and password: {}:{}'.format(user, password) 
  verified_user = user 
  verified_pass = password 
  # Use a regex to extract the timestamp value 
  m = re.search('timestamp=(.*)\"', r.text) 
  if m: 
    timestamp = int(m.group(1)) 
    print '[+] Found timestamp: {}'.format(timestamp) 
    # We have what we need, break out of the loop 
    break

With the timestamp in hand we need the code to craft the request, and send it! We already have the required form fields, and the correct URL for submitting our POST request from our previous analysis. Below shows the snippet of code responsible for enabling remote management on the router.

Python: enable remote management
# Page URL with timestamp from above 
page = 'apply.cgi?/FW_remote.htm%20timestamp={}'.format(timestamp) 
# Complete URL with username and password determined earlier 
url = 'http://{}:{}@{}/{}'.format(verified_user, verified_pass, target_ip, page) 
# Form data to enable remote management (extracted from Wireshark sniffing) 
# We include http_rmport as a variable so we can use a custom value in the future. 
data = { 'submit_flag': 'remote', 'http_rmenable': '1', 'local_ip': '...', 'remote_mg_enable': '0', 'rm_access': 'all', 'http_rmport': str(rmport) } 
# Submit the request! 
r = requests.post(url, headers=headers, data=data)

I ran the code and quickly popped over to my browser to check. Sure if enough, remote management was enabled!

A Complete Brute Force Router Takeover

Well, there you have it, a brute force router takeover. We have successfully developed a Python script to brute force basic authentication credentials on router administration pages. The script also enables remote management automatically. If you would like to download the code developed in this article head on over to Github and download it there: PORTAL on Github

If you make improvements to this code, or support additional routers, please submit a pull request. Drop me a comment below if you have any questions at all, or if you found this code useful!
So, are you still using default credentials on your router?

If you enjoyed reading this article I highly recommend checking out my other posts covering topics from Linux, to finance, and even horticulture!

Leave a Reply

Your email address will not be published. Required fields are marked *