Capture the Flag

Hack the Box Walkthrough: OpenSecret

OpenSecret Icon, courtesy of JippityToday, we’re going to tackle a Hack the Box Challenge called OpenSecret. Unlike the last few of these I’ve done, this is more of an offensive security challenge. Our challenge scenario is

A simple help desk portal where users can submit support tickets. The application uses JWT tokens for session management, but something seems off about how they’re implemented. Can you find the security flaw?


Task 1: Submit challenge Flag

After starting the challenge, you’ll be given a public IP to hit on a specific port, so no VPN access is required. For me, that IP:Port is 154.57.164.83:30250. Given the nature of the challenge (and that it is under the Category of “Web”), I know it will be a web application. Regardless, I did an nmap scan on just that port at that IP so I could know a little bit about it and it seems that this is a Node.js/Express application.

$ nmap -sCV -vv -p 30250 154.57.164.83                 
Starting Nmap 7.99 ( https://nmap.org ) at 2026-05-14 13:19 -0400
NSE: Loaded 158 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
Initiating Ping Scan at 13:19
Scanning 154.57.164.83 [4 ports]
Completed Ping Scan at 13:19, 0.02s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 13:19
Completed Parallel DNS resolution of 1 host. at 13:19, 0.49s elapsed
Initiating SYN Stealth Scan at 13:19
Scanning 154-57-164-83.static.isp.htb.systems (154.57.164.83) [1 port]
Discovered open port 30250/tcp on 154.57.164.83
Discovered open port 30250/tcp on 154.57.164.83
Completed SYN Stealth Scan at 13:19, 0.24s elapsed (1 total ports)
Initiating Service scan at 13:19
Scanning 1 service on 154-57-164-83.static.isp.htb.systems (154.57.164.83)
Completed Service scan at 13:19, 11.45s elapsed (1 service on 1 host)
NSE: Script scanning 154.57.164.83.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 5.13s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.75s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
Nmap scan report for 154-57-164-83.static.isp.htb.systems (154.57.164.83)
Host is up, received reset ttl 128 (0.028s latency).
Scanned at 2026-05-14 13:19:09 EDT for 17s

PORT      STATE SERVICE REASON          VERSION
30250/tcp open  http    syn-ack ttl 128 Node.js (Express middleware)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: OpenSecret Helpdesk - Support Portal

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 18.39 seconds
           Raw packets sent: 6 (240B) | Rcvd: 3 (128B)

Navigating to the site, we see this

OpenSecret Helpdesk Homepage

Given the description, I checked to see if there are any JWTs in storage already in the browser. I checked Cache Storage, Cookies, Indexed DB, Local Storage, and Session Storage, but nothing is there yet. Okay, I don’t see any other links, so I submitted the form with a name, email, and some pretend issue description words. Nothing fancy, no XSS attempts, etc. When I do, it tells me that no session token is provided.

OpenSecret No session token provided

Looking at the network call there, I just see this payload and these headers. No Cookies sent, and I don’t see an Auth Header.

{"name":"Bob Smith","description":"Blah blah blah.  All you ever do is say blah like things."}

OpenSecret Request Headers

Here is the response.

{"message":"No session token provided"}

So I need to see how this is being packaged up, so I take a look at the HTML source and … well, I guess the challenge is over. There is nothing really interesting in the HTML source until you get to the script tag at the end.

 <script>
    // JWT Secret Key
    const SECRET_KEY = "HTB{0p3n_s3cr3ts_ar3_n0t_s3cr3ts}";

    // Helper function to convert string to Base64URL
    function base64url(str) {
        return btoa(str)
            .replace(/\+/g, "-")
            .replace(/\//g, "_")
            .replace(/=/g, "");
    }

    // Generate a JWT session token for the user
    async function generateJWT() {
        // Check if user already has a token
        const existingToken = document.cookie
            .split("; ")
            .find((row) => row.startsWith("session_token="));

        if (existingToken) {
            console.log("Session token already exists");
            return;
        }

        // Create a random guest username
        const username = "guest_" + Math.floor(Math.random() * 10000);

        // JWT Header
        const header = { alg: "HS256", typ: "JWT" };

        // JWT Payload
        const payload = { username: username };

        // Encode header and payload
        const encodedHeader = base64url(JSON.stringify(header));
        const encodedPayload = base64url(JSON.stringify(payload));
        const data = encodedHeader + "." + encodedPayload;

        // Sign with SECRET_KEY using HMAC-SHA256
        const key = await crypto.subtle.importKey(
            "raw",
            new TextEncoder().encode(SECRET_KEY),
            { name: "HMAC", hash: "SHA-256" },
            false,
            ["sign"]
        );

        const signature = await crypto.subtle.sign(
            "HMAC",
            key,
            new TextEncoder().encode(data)
        );

        // Encode signature
        const encodedSignature = base64url(
            String.fromCharCode(...new Uint8Array(signature))
        );

        // Complete JWT token
        const token = data + "." + encodedSignature;

        // Store token in cookie
        document.cookie = `session_token=${token}; path=/; max-age=86400`;

        console.log("Generated session for:", username);
    }

    // Generate JWT token on page load
    generateJWT();

    // Handle ticket submission
    document
        .getElementById("submit-btn")
        .addEventListener("click", async (event) => {
            event.preventDefault();

            const name = document.getElementById("ticket-name").value;
            const description =
                document.getElementById("ticket-desc").value;

            const response = await fetch("/submit-ticket", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({ name, description }),
            });

            const result = await response.json();
            document.getElementById("message-display").textContent =
                result.message || "Ticket submitted successfully!";
        });
</script>

Task 1 Answer: HTB{0p3n_s3cr3ts_ar3_n0t_s3cr3ts}

That’s all there was to it. We don’t actually have to use that code and that “key” to impersonate anyone, this was just an easy example of the dangers of storing secrets openly (OHHHH, the room name makes so much sense now 😉 )

If you have any questions, let me know!

Leave a Reply

Your email address will not be published. Required fields are marked *