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.
Setup
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.
Step 1Inspect the auth cookie
ObservationThe description says the cookie holds an encrypted value and that the client can modify it. So inspect the cookie in DevTools and decode its base64 to see the ciphertext structure underneath.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.bashecho '<COOKIE_VALUE>' | base64 -d | base64 -d | xxdWhat 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 base64-encoded twice, so one decode gives you another base64 string rather than data. Pipe that to xxd and you see printable ASCII that still looks like base64, not raw ciphertext. Decode a second time to reach the actual AES bytes.
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: there is no dot-separated header, payload, and signature, so jwt.io reports an invalid token. It is raw AES-CBC ciphertext wrapped in base64, which calls for a hex viewer like xxd or CyberChef.
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 changeisAdmin=0toisAdmin=1without knowing the AES key.Step 2Perform a CBC bit-flipping attack
ObservationThe decoded ciphertext is a multiple of 16 bytes with no integrity tag, which points at AES-CBC. CBC bit-flipping can turn the isAdmin=0 byte into a 1 by XORing the matching 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.pythonpython3 - <<'EOF' import base64 cookie = "<PASTE_BASE64_COOKIE_HERE>" # the cookie is base64 twice over, so decode twice to reach the raw AES bytes ct = bytearray(base64.b64decode(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') # re-wrap in the same two layers before pasting it back into the cookie print(base64.b64encode(base64.b64encode(bytes(ct))).decode()) EOFWhat 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 each user's session record, so the offset of the 0 in isAdmin=0 shifts between instances and usernames. A fixed offset flips the wrong byte and corrupts some other field. Decode the cookie, read the plaintext structure, and compute the exact block and byte position from it.
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, so brute force is out of the question. The CBC bit-flip needs no key at all: it exploits the structure of CBC decryption, where each plaintext block is the block decryption XORed with the previous ciphertext block, so changing a byte there changes the corresponding plaintext byte.
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 positionjofC[i-1]with some deltad, positionjofP[i]flips by exactlyd. To forceP[i][j]from byteXto byteY, setd = X XOR Y.Worked example. Suppose the cookie decrypts to
username=guesty;admin=0arranged 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 y ; | <- bytes 0..15 Block 1: | a d m i n = 0 (padding) | <- 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 # Changing C[0] makes Decrypt(C[0]) unpredictable, so ALL 16 bytes of P[0] # turn to garbage, not just byte 6. That's fine here because the server only # validates admin=1 and never re-reads the username.Important caveat. Flipping a bit in
C[i-1]also scrambles all 16 bytes ofP[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.
Step 3Set the modified cookie and reload
ObservationThe server checks the cookie on every request to decide admin status. Replace the auth_name cookie with the bit-flipped base64 and reload, and the server decrypts it, reads isAdmin=1, and hands over 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_posby 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.