JAuth picoGym Exclusive Solution

Published: March 5, 2024

Description

JAuth stores session state in a JWT cookie. Because the token accepts alg=none, you can rewrite the payload to grant yourself admin access and bypass authentication.

JWT tampering

Visit http://saturn.picoctf.net:52680/ and log in with the provided test/Test123! credentials.

Open DevTools → Application → Cookies to copy the issued JWT. It contains three base64url segments separated by dots.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Decode the token
    Observation
    I noticed the session cookie was a long opaque string with two dots separating three segments, which is the hallmark structure of a JWT, suggesting I should decode the header and payload to see what claims control authorization.
    Base64-decode the first two segments. The header uses HS256 and the payload contains role:"user". Leave the signature segment blank for now.
    Learn more

    A JSON Web Token (JWT) is a compact, URL-safe token format used for authentication and authorization. It consists of three base64url-encoded segments separated by dots: header.payload.signature. The header specifies the token type and signing algorithm. The payload contains claims - key-value pairs like sub (subject), exp (expiration), and application-specific fields like role. The signature proves the header and payload have not been tampered with.

    Base64url is a variant of standard Base64 that replaces + with - and / with _, and omits padding (=). This makes the encoded string safe to include in URLs and cookies without percent-encoding. Decoding the token's first two segments gives you the raw JSON objects, which you can then read and modify.

    Tools like jwt.io (a web-based decoder) and the jwt command-line tool make decoding JWTs trivial. In a CTF, quickly decoding a JWT to inspect its payload is a standard first step whenever you see a suspiciously opaque cookie or authorization header - the structure is always the same three dot-separated segments.

  2. Step 2
    Forge an unsigned token
    Observation
    I noticed the decoded header specified 'alg':'HS256' and the payload contained 'role':'user', which suggested the server might accept the well-known 'alg:none' bypass, allowing me to rewrite the role claim to 'admin' without a valid signature.
    Change the header to {"typ":"JWT","alg":"none"} and the payload to set "role":"admin". Base64url-encode both segments without padding and concatenate them with a trailing dot to indicate an empty signature.
    bash
    printf '{"typ":"JWT","alg":"none"}' | base64 | tr -d '=' | tr '+/' '-_'
    bash
    printf '{"role":"admin", ...}' | base64 | tr -d '=' | tr '+/' '-_'

    Expected output

    eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0
    What didn't work first

    Tried: Omitting the trailing dot and submitting header.payload as the forged token

    The JWT spec requires three dot-separated segments, so most parsers see only two segments and reject the token as malformed before even checking the algorithm. The trailing dot is not decoration - it represents the empty signature field and is required for the token to parse as a valid three-part JWT.

    Tried: Using standard Base64 padding (with = characters) instead of stripping it for base64url

    Cookies and URLs treat = as a special character, so a padded base64 string in a cookie value is often corrupted or rejected by the server. Base64url requires padding to be stripped entirely; the decoder on the server infers the correct length from the segment itself.

    Learn more

    The alg:none vulnerability is a well-known JWT flaw. The JWT specification originally allowed "alg": "none" to indicate an unsigned token (useful in trusted, internal contexts). Vulnerable servers that accept this value skip signature verification entirely - meaning any client can forge a valid-looking token simply by setting the algorithm to none and providing no signature. The trailing dot in header.payload. represents the empty signature field.

    To forge the token: encode the modified header JSON with base64url (strip padding, swap + and /), do the same for the modified payload, then concatenate them as encodedHeader.encodedPayload. (note the trailing dot). This produces a syntactically valid JWT that many vulnerable libraries will accept as genuine because they check the algorithm field instead of enforcing that signatures are always required.

    This vulnerability was disclosed in 2015 and has affected real-world implementations in multiple languages. Properly hardened JWT libraries either reject alg:none entirely or require callers to explicitly allowlist acceptable algorithms. When reviewing any system that uses JWTs, checking that the server enforces algorithm restrictions is a standard item on the security review checklist. Related attacks include the algorithm confusion attack (switching RS256 to HS256 and signing with the public key) and header injection (pointing the jku or x5u header at an attacker-controlled key set).

  3. Step 3
    Swap the cookie
    Observation
    I noticed the admin page was gated purely on the JWT cookie value, which meant replacing the stored cookie with the forged 'alg:none' token in DevTools and reloading would let the server grant admin access without any further credentials.
    Replace the existing JWT cookie with the forged one (header.payload.) and refresh the page. The admin view appears immediately and prints the flag.
    Learn more

    Browser DevTools (F12) provides direct access to cookies via the Application tab. You can double-click any cookie value to edit it in place, then reload the page to send the new value to the server. This is the fastest way to test JWT forgeries without writing any exploit code - the browser handles the HTTP request with the modified cookie automatically.

    The fact that changing a single field in the JWT payload (role: "user" to role: "admin") immediately grants admin access illustrates a fundamental web security principle: never trust client-supplied data for authorization decisions. The server must verify both the token's signature and that the claims it contains are legitimate. Storing authorization state in an unverified client-side token is equivalent to having no authorization at all.

    In a real penetration test, JWT vulnerabilities are tested using tools like jwt_tool (a dedicated JWT attack suite), Burp Suite (intercept and modify tokens in the proxy), and custom scripts. Finding that a production application accepts alg:none tokens would be a critical severity finding - it completely bypasses authentication and allows full privilege escalation without knowing any credentials or secrets.

Alternate Solution

Paste your JWT into the JWT Decoder tool on this site to instantly see the decoded header and payload. This makes it easy to confirm the algorithm and identify which claim to modify before forging the token.

Flag

Reveal flag

picoCTF{succ3ss_@u7h3nt1c@710...4eacf}

JWTs that declare alg:"none" trust client-side data blindly, so editing the payload is enough to escalate privileges.

Key takeaway

JWTs are not encrypted and their payload is freely readable by anyone who holds the token. The security guarantee comes entirely from the signature, which proves the server issued the exact header and payload without modification. When a server accepts 'alg:none' it skips signature verification, allowing any client to forge arbitrary claims including privilege fields like 'role'. Related attacks such as algorithm confusion (RS256 to HS256) and JKU header injection follow the same root cause: trusting algorithm selection or key references supplied by the token itself rather than enforcing them server-side.

Related reading

Want more picoGym Exclusive writeups?

Useful tools for Web Exploitation

What to try next