Easy Peasy

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.

nc mercury.picoctf.net <PORT>

Solution

  1. Step 1Get 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.

  2. Step 2Exhaust 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.

  3. Step 3Retrieve 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.

  4. Step 4Recover 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.
    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.

Flag

picoCTF{...}

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

More Cryptography