Java Code Analysis!?! picoCTF 2023 Solution

Published: April 26, 2023

Description

A Spring Boot REST API issues JWT tokens for authentication. The source code is provided. Find the JWT signing secret, forge a token with an elevated role claim, and use it to access the admin-only endpoint that reveals the flag.

Download and unzip the source code.

Start the web application as instructed or connect to the provided URL.

Optionally install the jwt-cli tool or use jwt.io to decode and forge tokens.

bash
wget https://artifacts.picoctf.net/c/494/bookshelf-pico.zip && unzip bookshelf-pico.zip
bash
pip3 install pyjwt
  1. Step 1Find the JWT secret in the source code
    Grep the source for secret-related keywords. The hardcoded value will look something like a string assignment, e.g. secret = "verysecretkey" inside a config class or properties file.
    bash
    grep -rn 'secret\|SECRET\|jwtKey\|signing' . --include='*.java' --include='*.properties' --include='*.yml'
    Learn more

    Spring Boot applications that use JSON Web Tokens (JWT) must sign each token with a secret key to prevent tampering. This secret is typically stored in application.properties, application.yml, or as a constant in a configuration class. Hardcoding secrets in source code is a critical security flaw: if the code is ever leaked or open-sourced, every token the application issues can be forged.

    In CTF challenges providing source code, a targeted grep for common secret-related keywords will quickly surface the value. Look for a literal assignment like private static final String SECRET = "verysecretkey"; or a properties line such as jwt.secret=verysecretkey. In real-world code reviews this is also the first thing security auditors check.

  2. Step 2Decode the existing JWT and verify the role claim
    Log in with a regular account to obtain a token, then base64-decode the payload. Confirm role: "USER" exists (so you know the field name) and check the exp claim before forging.
    bash
    curl -s -X POST http://<HOST>/login -H 'Content-Type: application/json' -d '{"username":"user","password":"user"}' | jq .
    bash
    base64 -d <<< '<JWT_PAYLOAD_BASE64>'
    Learn more

    A JWT has three dot-separated base64url-encoded parts: header, payload, and signature. The payload contains claims like sub (subject/username), exp (expiration), and application-specific claims such as role.

    To decode manually: split the token on ., take the middle section (payload), pad it to a multiple of 4 characters, and base64-decode it. You will see JSON like { "sub": "user", "role": "USER", "exp": 1234567890 }. Two things to verify before forging: (1) confirm the payload actually contains role: "USER" so you know the exact claim name to flip to ADMIN, and (2) note the exp value. Spring Security rejects tokens with bogus expirations, so your forged token must use a current Unix timestamp + a positive offset (e.g. now + 3600).

    You can also use jwt.io to inspect tokens visually, or read the JWT and cookie attacks for CTF guide for the full forge-and-replay workflow.

  3. Step 3Forge a JWT with role: ADMIN
    Using the discovered secret, sign a new token with the admin role claim using PyJWT or jwt.io.
    python
    python3 -c "
    import jwt, time
    payload = {'sub': 'user', 'role': 'ADMIN', 'exp': int(time.time())+3600}
    token = jwt.encode(payload, '<SECRET>', algorithm='HS256')
    print(token)
    "
    Learn more

    JWT signatures use HMAC-SHA256 (HS256) or RSA (RS256). When the secret is known, forging a token with arbitrary claims is trivial: construct the header and payload JSON, sign with the secret, and concatenate. The server cannot distinguish your forged token from a legitimate one because it only verifies the signature - it trusts the claims inside.

    This is why secrets must be truly secret and preferably long random values. The OWASP JWT Security Cheat Sheet recommends at least 256-bit secrets for HS256. Additionally, important claims like role should ideally be looked up from the database on each request rather than trusted directly from the token.

  4. Step 4Access the admin endpoint to retrieve the flag
    Send a request to the admin-only route with the forged token in the Authorization header.
    bash
    curl -H 'Authorization: Bearer <FORGED_TOKEN>' http://<HOST>/admin/flag
    Learn more

    With a valid-looking token bearing the ADMIN role, the Spring Security filter chain will grant access to routes annotated with @PreAuthorize("hasRole('ADMIN')") or equivalent. The admin endpoint will return the flag.

    This challenge demonstrates one of the most common JWT pitfalls: trusting claims inside the token without server-side authorization checks. The correct approach is to store roles and permissions in a database and look them up by the token's subject on every request, treating the JWT only as an authentication credential, not an authorization source.

Flag

picoCTF{...}

This challenge was not solved during the competition. Follow the steps above to reproduce the solution.

Want more picoCTF 2023 writeups?

Useful tools for Web Exploitation

Related reading

What to try next