Easy Peasy picoCTF 2021 Solution

Published: April 2, 2026

Description

A one-time pad is truly unbreakable... as long as you never reuse the key. Connect to the server and break the OTP.

Remote

Connect via netcat.

bash
nc mercury.picoctf.net <PORT>
Background reading: Stream Ciphers in CTFs explains why keystream reuse breaks XOR-based pads, and AES for CTF covers the block-cipher counterpart.
  1. Step 1Identify the key length
    Read the provided source - the key length is almost always a hardcoded constant (here, 40000). If only the binary is available, check any key file size, or send a long known plaintext and watch where the ciphertext starts repeating.
    bash
    grep -n -i 'key.*=' easy-peasy.py 2>/dev/null
    bash
    wc -c key 2>/dev/null
    Learn more

    Why key length matters. When the keystream cycles before your message ends, ciphertext at offset i and offset i + L share the same key byte (where L is the period). If you control either plaintext, XORing the two ciphertexts cancels the key entirely: c1 XOR c2 = p1 XOR p2. The whole exploit depends on knowing the period exactly.

  2. Step 2Get the encrypted flag
    Connect to the server. It immediately sends the encrypted flag as a hex string - this is the flag XORed with the key starting at offset 0. Save this value.
    Learn more

    A one-time pad (OTP) is a theoretically unbreakable encryption scheme where the key is as long as the message and is used exactly once. Encryption is performed by XORing each plaintext byte with the corresponding key byte: ciphertext = plaintext XOR key. Because XOR is its own inverse, decryption uses the identical operation: plaintext = ciphertext XOR key.

    The fundamental rule is that the key must never be reused. If the same key bytes encrypt two different plaintexts, an attacker who knows one plaintext can recover the other: c1 XOR c2 = p1 XOR p2. This vulnerability is why "one-time" is in the name - the moment the key is reused, the security guarantee evaporates entirely.

    The server sends the flag encrypted at key offset 0. Your goal is to force the server to reveal those same key bytes so you can undo the encryption.

  3. Step 3Exhaust the key
    The key is exactly 40000 bytes and the server tracks a usage offset. Send exactly 40000 bytes of known plaintext (e.g., all zeros). This advances the offset all the way to the end, causing it to wrap back to 0.
    Learn more

    The server maintains a global offset into its 40000-byte key and increments it with every encryption request. By sending exactly 40000 bytes, you push the offset from its current position to the end of the key buffer, at which point it wraps back to 0 - returning to the same key bytes that encrypted the original flag.

    Sending all-zero bytes is the ideal known plaintext: 0x00 XOR key_byte = key_byte, meaning the server's response is simply the raw key bytes. This is a classic known-plaintext attack - you choose the message, so you immediately know the relationship between input and output.

    The 40000-byte input needs to be sent as a hex string because the server reads hex-encoded input. The response (the "encrypted" zeros) is the key itself, but since you're just draining the buffer here, you discard it.

  4. Step 4Retrieve the key at offset 0
    Now ask the server to encrypt another 50-byte block of known plaintext (e.g., all zeros). Since the offset is back at 0, the server XORs your zeros with the first 50 bytes of the key - giving you the raw key bytes.
    Learn more

    With the offset reset to 0, encrypting all-zero bytes causes the server to output 0x00 XOR key[0..49] - which is simply the first 50 bytes of the key, in plain view. This is the same portion of the key that was used to encrypt the original flag, so you now have everything needed to reverse the encryption.

    This technique - deliberately controlling the server's state to expose key material - is a form of chosen-plaintext attack. You're not breaking the cipher mathematically; you're exploiting a stateful design flaw that allows key reuse.

  5. Step 5Recover the flag
    XOR the encrypted flag bytes with the recovered key bytes. Because XOR is its own inverse and the key is the same, you get the original plaintext.
    python
    python3 << 'EOF'
    from pwn import *
    
    io = remote("mercury.picoctf.net", <PORT>)
    
    # Step 1: get encrypted flag
    enc_flag = bytes.fromhex(io.recvline().strip().decode())
    
    # Step 2: exhaust the 40000-byte key with known zeros
    chunk = b'\x00' * 40000
    io.sendline(chunk.hex().encode())
    io.recvline()  # discard server response
    
    # Step 3: encrypt zeros at offset 0 to learn the key
    io.sendline((b'\x00' * len(enc_flag)).hex().encode())
    key_bytes = bytes.fromhex(io.recvline().strip().decode())
    
    # Step 4: XOR to recover the flag
    flag = bytes([a ^ b for a, b in zip(enc_flag, key_bytes)])
    print(flag.decode())
    io.close()
    EOF
    Learn more

    pwntools is the standard Python library for CTF exploitation. The remote() function opens a TCP connection and provides sendline() / recvline() methods to interact with the server programmatically. This makes it trivial to script multi-step protocol interactions like the key exhaustion attack here.

    The final XOR is performed byte-by-byte using a list comprehension with zip(), which pairs each encrypted byte with the corresponding key byte. XOR is commutative and self-inverse: (plaintext XOR key) XOR key = plaintext. As long as the key bytes are identical between the two encryptions - which the wrap-around guarantees - the flag is recovered perfectly.

    Real-world lesson: Stateful key streams are dangerous whenever the state can be manipulated externally. Real OTP systems use hardware random number generators and destroy the key material after a single use. Stream ciphers like RC4 famously suffered related vulnerabilities when used in WEP WiFi encryption, where the same keystream was reused across packets.

Alternate Solution

After forcing the key reuse and obtaining the XOR-encrypted ciphertext, use the XOR Cipher tool on this site to perform the final decryption - paste the hex ciphertext, enter the known key bytes, and the plaintext flag appears instantly without writing any Python.

Flag

picoCTF{...}

The key is 40000 bytes and cycles - send exactly 40000 bytes to force a key reuse, then exploit the known-plaintext.

Want more picoCTF 2021 writeups?

Useful tools for Cryptography

Related reading

What to try next