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.
Setup
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.
wget https://artifacts.picoctf.net/c/494/bookshelf-pico.zip && unzip bookshelf-pico.zippip3 install pyjwtSolution
Walk me through it- Step 1Find the JWT secret in the source codeGrep 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
grepfor common secret-related keywords will quickly surface the value. Look for a literal assignment likeprivate static final String SECRET = "verysecretkey";or a properties line such asjwt.secret=verysecretkey. In real-world code reviews this is also the first thing security auditors check. - Step 2Decode the existing JWT and verify the role claimLog 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 .bashbase64 -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 asrole.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 containsrole: "USER"so you know the exact claim name to flip toADMIN, and (2) note theexpvalue. 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.
- Step 3Forge a JWT with role: ADMINUsing 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
roleshould ideally be looked up from the database on each request rather than trusted directly from the token. - Step 4Access the admin endpoint to retrieve the flagSend 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/flagLearn more
With a valid-looking token bearing the
ADMINrole, 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.