Hammer — TryHackMe Writeup
Nmap exposes SSH and a custom web app on port 1337. Fuzzing with the hmr_ prefix leaks an open log that dumps a valid username.
Bypassed the password reset rate-limit with X-Forwarded-For spoofing, brute-forced the 4-digit recovery code, and reset the password to grab the user flag.
Forged an admin HS256 token using an exposed key, then abused execute_command.php for command execution to retrieve the root flag.
Introduction
Hammer is a medium-rated Linux machine that front-ends a small custom PHP web app on a non-standard HTTP port (1337). The application features a login portal with a "forgot password" flow. Our objective is to capture two flags:
- ▹A user flag located behind the authenticated dashboard.
- ▹A root flag readable only after escalating to an admin role and triggering remote command execution.
The entire attack chain is web-based—no SSH credentials are required for the intended path.
Reconnaissance
::Nmap Scan
A full TCP port scan reveals two listening services:
Only two ports are open: SSH and a custom Apache web application on port 1337. The PHPSESSID
cookie lacks the HttpOnly flag.
Browsing to http://10.113.158.252:1337/ returns a login page. An HTML comment found in the page source provides a small but crucial hint:
This hmr_ prefix serves as an excellent anchor for directory fuzzing.
::Web Enumeration
Using the hmr_ convention, we can fuzz for hidden directories:
This reveals several directories: hmr_css, hmr_js, hmr_images, and most importantly, hmr_logs.
::The Leak: hmr_logs/error.logs
The hmr_logs/ directory has auto-indexing enabled, and the error.logs file is world-readable. Digging through the Apache error lines uncovers a critical piece of information:

We now have a valid username: tester@hammer.thm. The next step is to abuse the password reset functionality.
Initial Foothold
::Understanding the Reset Flow
Accessing reset_password.php with the leaked email returns a secondary form asking for a 4-digit recovery code, alongside a hidden s field acting as a client-side countdown timer. Two protections stand out:
- ▹Per-session rate limit — visible in the
Rate-Limit-Pendingresponse header. - ▹Client-side timer — the
svalue is merely a number that the browser counts down; the server trusts whatever value we send.

::Bypassing Rate Limiting and Timers
Two Bugs, One Exploit: Spoofing the X-Forwarded-For header with a random IP defeats the
IP-based rate limiter, and submitting a massive s value (e.g., 999999999) neutralizes the
client-side countdown. Rotating the PHP session cookie every few attempts prevents the per-session
counter from blocking our requests.
A short Python script can quickly iterate through the 0000–9999 space to brute-force the recovery code:
The script successfully stops at 1034. With the correct code accepted, we are redirected to the Set New Password form.

::Login & User Flag
Logging in with our newly set credentials assigns us a token cookie (which is a JWT) and redirects us to the dashboard:
The dashboard greets us with Welcome, Thor! and presents the user flag:

Privilege Escalation
::Anatomy of the Issued Token
Decoding the token cookie from the dashboard reveals the following structure:

Header:
Payload:
Two key observations:
- ▹The algorithm is symmetric (HS256), meaning the same secret is used to both sign and verify the token.
- ▹Our current role is
user. We need to elevate this toadminto interact with the command execution endpoint.
::Analyzing the Dashboard
The dashboard provides a command execution interface. Attempting to run standard commands like pwd results in a "Command not allowed" error.

However, testing other basic commands reveals that ls executes successfully:

Most of these files are standard or expected, but 188ade1.key stands out.
::Recovering the Signing Secret
Since 188ade1.key is located in the web root, we can fetch it directly:
This 32-character string is the HMAC secret used to sign the JWTs.
::Forging the Admin Token
With the secret in hand, we can forge a new token. We will update the payload to set the role to admin, keep the kid pointing to the absolute path /var/www/html/188ade1.key, and sign it using the recovered secret string.
You can use a proxy like Caido or Burp Suite, online tools like jwt.io, or a custom Python script to generate the forged JWT.

::Triggering RCE via execute_command.php
The dashboard sends POST requests to execute_command.php, which validates whether the role is admin. We can replay this request using our newly forged bearer token:
Because the application is vulnerable to OS command injection via this endpoint, any command we send will be executed by the web service account. To read the root flag:
Manual Execution: You can easily send the forged Authorization: Bearer <ADMIN_JWT> request to execute_command.php using a tool like Caido or Burp Suite Repeater. Include the JSON body {"command":"cat /home/ubuntu/flag.txt"} to retrieve the flag.

Root Flag:
Summary
Hammer is an excellent demonstration of chaining small web vulnerabilities to achieve complete system compromise:
- ▹An open log directory (
hmr_logs/error.logs) leaked a valid username. - ▹A recovery-code reset with a small 4-digit keyspace and easily bypassable rate-limiting allowed for account takeover.
- ▹A symmetrically-signed JWT paired with an exposed HMAC secret enabled us to forge an admin token, ultimately leading to OS command execution.

Red Team Consultant · Penetration Tester · Bug Bounty Hunter
Offensive security professional with 250+ vulnerabilities reported across 50+ organizations including Atlassian, Vimeo, and AT&T. Sharing research, tools, and field notes.