More Cookies picoCTF 2021 Solution

Published: April 2, 2026

Description

I forgot Cookies can Be modified Client-side! Edit the cookies to log in as admin.

The cookie contains an encrypted value - but the encryption mode is vulnerable.

Open the challenge URL in your browser and inspect the cookies with DevTools.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
Companion reading: Cookies and JWTs in CTFs explains client-side session-token tampering, and AES for CTF covers the CBC mode (and its bit-flipping flaw) used here.
  1. Step 1
    Inspect the auth cookie
    Observation
    I noticed the challenge description explicitly says the cookie contains an encrypted value and is modifiable client-side, which suggested inspecting the cookie directly in DevTools and decoding its base64 contents to understand the underlying ciphertext structure.
    Open browser DevTools (F12) > Application > Cookies and find the auth_name cookie. It will be a base64-encoded string. The cookie is double base64 encoded - decode once to get another base64 string, then decode again to see the raw ciphertext bytes.
    bash
    echo '<COOKIE_VALUE>' | base64 -d | base64 -d | xxd
    What didn't work first

    Tried: Decoding the cookie only once with base64 -d and then trying to read it as plaintext JSON or a JWT.

    The cookie is double base64 encoded, so a single decode yields another base64 string, not readable data. Piping it to xxd at that stage shows printable ASCII characters that look like more base64, not raw ciphertext bytes. A second decode is required to reach the actual AES ciphertext.

    Tried: Pasting the cookie into a JWT debugger like jwt.io expecting to see a header and payload.

    The auth_name cookie is not a JWT - it has no dot-separated header.payload.signature structure. jwt.io will report an invalid token. The cookie is raw AES-CBC ciphertext wrapped in base64, so the correct tool is a hex viewer (xxd or CyberChef) to inspect the raw bytes.

    Learn more

    The cookie value is base64-encoded ciphertext produced by AES in CBC mode (Cipher Block Chaining). In CBC mode, the plaintext is divided into 16-byte blocks, each block is XORed with the previous ciphertext block before being encrypted. The first block is XORed with a random initialization vector (IV).

    The plaintext likely encodes a user record like isAdmin=0;username=user. Your goal is to change isAdmin=0 to isAdmin=1 without knowing the AES key.

  2. Step 2
    Perform a CBC bit-flipping attack
    Observation
    I noticed the decoded ciphertext length was a multiple of 16 bytes with no integrity tag, which indicated AES-CBC mode and suggested using the CBC bit-flip property to change the 'isAdmin=0' byte to '1' by XORing the corresponding position in the preceding ciphertext block.
    Identify which byte in the ciphertext corresponds to the '0' in 'isAdmin=0'. XOR that ciphertext byte with (ord('0') XOR ord('1')) to flip the corresponding plaintext bit in the next block.
    python
    python3 - <<'EOF'
    import base64
    
    cookie = "<PASTE_BASE64_COOKIE_HERE>"
    ct = bytearray(base64.b64decode(cookie))
    
    # Find the byte position in the previous ciphertext block that
    # corresponds to the '0' in the plaintext of the next block.
    # XOR that position with ord('0') ^ ord('1') = 1
    target_pos = <OFFSET_IN_PREVIOUS_BLOCK>
    ct[target_pos] ^= ord('0') ^ ord('1')
    
    print(base64.b64encode(bytes(ct)).decode())
    EOF
    What didn't work first

    Tried: Hardcoding target_pos = 6 without verifying the actual byte layout of the cookie for this specific instance.

    The plaintext layout depends on how the server formats the session record for each user, so the offset of the '0' in 'isAdmin=0' varies between instances and usernames. Using a fixed offset without decoding the cookie first will flip the wrong byte, corrupting a different field. The correct approach is to base64-decode the cookie, read the plaintext structure from context, and compute the exact block and byte position from the known plaintext.

    Tried: Attempting to decrypt the cookie by brute-forcing the AES key instead of using the bit-flip property.

    AES-128 has a 128-bit key space, making brute force computationally infeasible. The CBC bit-flip attack does not require the key at all - it exploits the mathematical structure of CBC decryption (P[i] = Decrypt(C[i]) XOR C[i-1]) to modify a known plaintext byte by XORing the corresponding position in the previous ciphertext block.

    Learn more

    The CBC bit-flip identity. In CBC decryption, each plaintext block is computed as: P[i] = Decrypt(C[i]) XOR C[i-1]. So if you XOR position j of C[i-1] with some delta d, position j of P[i] flips by exactly d. To force P[i][j] from byte X to byte Y, set d = X XOR Y.

    Worked example. Suppose the cookie decrypts to username=guest;admin=0 arranged in 16-byte blocks like this (block boundaries marked with |):

    Block 0:  | u s e r n a m e = g u e s t ; |   <- bytes 0..15
    Block 1:  | a d m i n = 0 \0 \0 \0 \0 \0 \0 \0 \0 \0 |   <- bytes 16..31
                        ^
                        target: byte 22 (the '0' to flip to '1')

    The target byte sits at position 6 of plaintext block 1. To flip it, modify byte 6 of ciphertext block 0 (the previous block):

    delta = ord('0') ^ ord('1') = 0x30 ^ 0x31 = 0x01
    
    ct[6] ^= 0x01   # flips '0' -> '1' in P[1] at position 6
    
    # But this also corrupts P[0] at position 6 (now random byte instead of 'm').
    # That's fine here because the server only validates admin=1, not username.

    Important caveat. Flipping a bit in C[i-1] also scrambles all 16 bytes of P[i-1] when you control only one byte (the AES decryption of a modified ciphertext block produces 16 unrelated bytes). The attack is only useful when you can tolerate garbage in one plaintext block to achieve a targeted change in the next - typical for cookies where the username block is non-critical and the admin block is the gate.

    Defense: CBC mode without authentication is vulnerable to this attack. The solution is to use an authenticated encryption mode like AES-GCM or to append an HMAC to the ciphertext. Any modification to the ciphertext then causes authentication to fail before decryption even occurs.

  3. Step 3
    Set the modified cookie and reload
    Observation
    I noticed the server validates the cookie on each request to determine admin status, which suggested that replacing the auth_name cookie with the bit-flipped base64 value and reloading would cause the server to decrypt it and read isAdmin=1, granting access to the flag.
    Replace the auth_name cookie value with the modified base64 string from the previous step. Reload the page - if the bit-flip is correct, the server will decrypt the cookie and see isAdmin=1, granting admin access and displaying the flag.
    Learn more

    This may require a few attempts to get the byte offset exactly right. If the page shows an error or logs you out, adjust target_pos by one and try again. The correct offset is determined by the exact byte position of the character you want to change within its plaintext block (0 to 15).

Interactive tools
  • Flask Session DecoderDecode Flask / itsdangerous session cookies. Splits payload, decompresses zlib, parses JSON, and verifies the HMAC signature when given the secret.

Flag

Reveal flag

picoCTF{cO0ki3s_yum_...}

AES-CBC without authentication is vulnerable to bit-flipping attacks - flipping a ciphertext byte predictably flips a plaintext bit in the next block, enabling privilege escalation without knowing the key.

Key takeaway

CBC mode provides confidentiality but no integrity: because each plaintext block is derived from the previous ciphertext block via XOR, an attacker who knows the current plaintext byte can compute the exact ciphertext delta needed to substitute any desired byte in the next block. Authenticated encryption modes like AES-GCM bind ciphertext to an authentication tag, making any modification detectable before decryption. The same bit-flipping primitive is also used in CBC padding oracle attacks, where the corruption is used to probe padding validity byte by byte.

Related reading

Want more picoCTF 2021 writeups?

Tools used in this challenge

What to try next