Skip to main content

JaWT Scratchpad picoCTF 2019 Solution

A web challenge involving forged authentication tokens to gain unauthorized access as an admin user.

Published: April 2, 2026Updated: August 13, 2026

Description

Use the JaWT Scratchpad application. The admin user has the flag.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Log in and capture the JWT
    Observation
    The challenge is named 'JaWT Scratchpad' and the description says the admin user holds the flag. So the app authenticates with JSON Web Tokens, and capturing our own token after login shows the structure we need to forge.
    Register or log in as any user (e.g., 'guest'). After login, inspect your cookies in browser DevTools. You will find a JWT (JSON Web Token) cookie - it looks like three base64url segments separated by dots.
    Learn more

    A JWT has three parts: header.payload.signature. Each part is base64url-encoded. The header specifies the algorithm (e.g., HS256), the payload contains claims like the username, and the signature verifies integrity.

    The payload is not encrypted - only signed. Anyone can decode and read the claims, but modifying them invalidates the signature (unless you can forge it).

  2. Step 2Decode the JWT
    Observation
    The JWT cookie has three dot-separated base64url segments. The middle one is the payload, which should hold the username claim that controls admin access, so decode that.
    Paste your JWT into jwt.io or use Python to base64-decode the payload. You will see something like {"user": "guest"}. Your goal is to change this to {"user": "admin"}.
    python
    python3 -c "
    import base64, json
    token = 'YOUR.JWT.HERE'
    payload = token.split('.')[1]
    # Add padding if needed
    payload += '=' * (4 - len(payload) % 4)
    print(json.loads(base64.urlsafe_b64decode(payload)))
    "
    What didn't work first

    Tried: Decode all three JWT segments at once by splitting on dots and decoding each with base64.b64decode.

    The header and signature segments decode fine, but the payload uses base64url, where '+' becomes '-' and '/' becomes '_'. Feeding that to base64.b64decode raises a binascii.Error on any token containing those characters. Use base64.urlsafe_b64decode, and add '=' padding to the payload segment.

    Tried: Paste the JWT into a generic base64 decoder website to read the payload.

    Generic decoders treat the whole dot-joined token as one blob and either refuse it or return garbage. A JWT has three separate segments, and only the middle one is the payload. Use jwt.io, or split on '.' and decode index 1.

    Learn more

    Base64url is a URL-safe variant of base64 that replaces + with - and / with _ and omits padding =. You may need to add padding back when decoding manually.

  3. Step 3Crack the HMAC secret with hashcat
    Observation
    The JWT header says HS256, and the server rejects alg:none tokens. So the only way to forge an admin token is recovering the symmetric HMAC secret, via an offline dictionary attack with hashcat mode 16500.
    Save your JWT to a file, then run hashcat in JWT mode (-m 16500) against the rockyou wordlist. The secret key turns out to be 'ilovepico'. The server enforces HS256 and does not accept unsigned (alg:none) tokens, so key cracking is the only working path.
    bash
    # Save your token to a file (one token per line)
    echo 'YOUR.JWT.HERE' > token.txt
    bash
    hashcat -a 0 -m 16500 token.txt /usr/share/wordlists/rockyou.txt
    bash
    # After hashcat finishes, confirm the result
    hashcat -a 0 -m 16500 token.txt /usr/share/wordlists/rockyou.txt --show

    Expected output

    YOUR.JWT.HERE:ilovepico
    What didn't work first

    Tried: Try the alg:none attack by changing the header algorithm to 'none' and stripping the signature, then submitting the modified token.

    The server validates the algorithm field and rejects tokens claiming alg:none, so it errors rather than accepting an unsigned token. The challenge detail says as much: HS256 is enforced and unsigned tokens are refused. Recovering the actual HMAC secret by dictionary attack is the only path.

    Tried: Run hashcat with mode -m 1400 (plain SHA-256) instead of -m 16500 (JWT HS256).

    Mode 1400 hashes the raw input as SHA-256 with no JWT structure, so it never produces a candidate matching the signature, whatever wordlist you use. Mode 16500 is built for JWTs: it computes HMAC-SHA256 over base64url(header) + '.' + base64url(payload) with each candidate and compares against the embedded signature. The wrong mode returns zero results even when the key is in the list.

    Learn more

    HS256 (HMAC-SHA256) signs the JWT by computing HMAC-SHA256(base64url(header) + "." + base64url(payload), secret). The signature is valid only if you know the secret. Because the secret is symmetric and shared, a weak secret can be brute-forced offline: you just try every candidate word and check whether it reproduces the observed signature.

    Hashcat mode 16500 handles the JWT format natively. Given the full token string, it will iterate through the wordlist and stop when it finds the key that makes the signature match. The recovered key here is ilovepico.

    John the Ripper is an alternative: convert the JWT to John format with a helper script, then run john jwt.john --wordlist=rockyou.txt.

  4. Step 4Forge the admin token and get the flag
    Observation
    hashcat recovers the secret 'ilovepico'. With that in hand, a modified payload setting user to admin can be re-signed with PyJWT or jwt.io into a token the server will accept.
    With the secret 'ilovepico' in hand, go to jwt.io, paste your original token, change the payload to {"user": "admin"}, and enter 'ilovepico' in the secret field. The debugger will produce a correctly signed admin token. Alternatively, use PyJWT in Python. Copy the resulting token, replace your jwt cookie value in DevTools (Application > Cookies), and reload the page to see the flag.
    python
    python3 -m pip install PyJWT
    python
    python3 -c "
    import jwt
    token = jwt.encode({'user': 'admin'}, 'ilovepico', algorithm='HS256')
    print(token)
    "
    What didn't work first

    Tried: Use jwt.encode without specifying algorithm='HS256', relying on the PyJWT default.

    PyJWT 2.x requires the algorithm parameter; leave it out and you get a TypeError and no token. PyJWT 1.x defaulted to HS256, but you may not know which version is installed. Always pass algorithm='HS256' explicitly.

    Tried: Manually base64url-encode a modified payload and concatenate header.payload with the original signature instead of re-signing.

    The original signature was computed over the old header and payload. Change the payload to set user=admin and those encoded bytes differ, so the old signature no longer matches and the server rejects the token. Recompute HMAC-SHA256 over the new header and payload using the recovered secret 'ilovepico'.

    Learn more

    Re-signing with the recovered key proves that the entire security model of this app rests on the secrecy of that one word. A strong, randomly generated secret (at least 256 bits) would make an offline dictionary attack computationally infeasible.

    This challenge illustrates why JWT secrets must be treated like passwords: long, random, and never reused. The countermeasure is straightforward: use a cryptographically random secret at deploy time and store it in an environment variable, not in source code.

Interactive tools
  • JWT DecoderDecode JSON Web Tokens and inspect the header, payload, and signature. Useful for web exploitation challenges.
Alternate Solution

Use the JWT Decoder tool on this site to decode and inspect the token's header and payload in one click. Once you confirm the payload claims, crack the HMAC secret with John the Ripper instead of hashcat: convert the token to John format with a helper script, then run john jwt.john --wordlist=rockyou.txt. The recovered secret ilovepico can then be plugged into jwt.io to re-sign the admin payload.

Flag

Reveal flag

picoCTF{jawt_was_just_what_you_thought_...}

Forge a JWT with user=admin by cracking the weak HMAC secret ('ilovepico') with hashcat or John the Ripper, then re-sign the token.

Key takeaway

JSON Web Tokens use a symmetric HMAC secret to guarantee the payload was not tampered with, but if that secret is a dictionary word an attacker recovers it offline by testing which candidate reproduces the observed signature. Once it is known, any payload can be forged and signed correctly, elevated privilege claims included. The same offline cracking threat hangs over anything where a shared secret signs a predictable, attacker-visible structure: cookie MACs, API tokens, and signed session blobs.

Related reading

Useful tools for Web Exploitation

Where to go next