rotation picoCTF 2023 Solution

Published: April 26, 2023

Description

A single text file contains an encrypted string; the challenge name hints at a Caesar/ROT-style cipher. Discover the shift that restores the flag.

Download encrypted.txt and read the ciphertext (e.g. xqkwKBN{...}).

Brute-force every shift from 1..25 in Python and look for the row that begins with picoCTF.

bash
wget https://artifacts.picoctf.net/c/354/encrypted.txt
bash
cat encrypted.txt
python
python3 - <<'PY'
from pathlib import Path
cipher = Path('encrypted.txt').read_text().strip()
for shift in range(1, 26):
    plain = []
    for ch in cipher:
        if 'a' <= ch <= 'z':
            plain.append(chr((ord(ch) - 97 - shift) % 26 + 97))
        elif 'A' <= ch <= 'Z':
            plain.append(chr((ord(ch) - 65 - shift) % 26 + 65))
        else:
            # digits, braces, underscores, punctuation pass through unchanged
            plain.append(ch)
    line = ''.join(plain)
    if line.lower().startswith('picoctf'):
        print(f'shift {shift}: {line}')
PY

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
Caesar cipher with 25 candidate shifts: brute force is the right tool. For the broader picture on Caesar, ROT13, and related encodings, see the CTF encodings guide.
  1. Step 1
    Read the cipher from the file
    Observation
    I noticed the challenge provides a single file, encrypted.txt, which must be loaded before any decryption can happen, so reading it into a variable is the necessary first action before attempting any shift.
    Path('encrypted.txt').read_text().strip() avoids hardcoding the ciphertext into the script and survives if the file gets re-issued with a different example.
    Learn more

    Hardcoding the ciphertext into the brute-force script is fine for a one-off, but reading the file is the cleaner habit: cipher = Path('encrypted.txt').read_text().strip(). strip() trims the trailing newline that read_text includes, so the loop does not see \n as a non-alphabetic passthrough byte and waste a comparison.

  2. Step 2
    Brute-force all 25 shifts
    Observation
    I noticed the challenge name explicitly says 'rotation' and the ciphertext characters are all shifted letters like 'xqkwKBN', which identified this as a Caesar cipher and meant the only unknown was the shift value, making exhaustive brute force over all 25 candidates the fastest approach.
    Caesar has only 25 non-trivial shifts. Print every candidate and the unique line beginning with picoCTF is the answer.
    python
    python3 - <<'PY'
    from pathlib import Path
    cipher = Path('encrypted.txt').read_text().strip()
    for shift in range(1, 26):
        plain = ''.join(
            chr((ord(c) - 97 - shift) % 26 + 97) if 'a' <= c <= 'z'
            else chr((ord(c) - 65 - shift) % 26 + 65) if 'A' <= c <= 'Z'
            else c
            for c in cipher
        )
        print(f'{shift:>2}: {plain}')
    PY

    Expected output

    shift  8: picoCTF{r0tat1o...d140864}
    What didn't work first

    Tried: Use ROT13 (shift 13) directly since that is the most common rotation cipher seen in CTFs.

    ROT13 only works when the encryption shift is exactly 13. Applying a fixed ROT13 to a ROT8-encoded ciphertext produces a different wrong plaintext (effectively ROT5 back from the original). The brute-force loop over all 25 shifts is necessary because the problem gives no information about which offset was used.

    Tried: Rotate digits along with letters so the entire ASCII printable range shifts uniformly.

    Rotating digits breaks the flag structure: if the digit '0' shifts by 8 it becomes '8', so the body of the flag becomes unreadable even at the correct letter shift of 8. The Caesar cipher only rotates alphabetic characters; digits, braces, underscores, and other punctuation must pass through unchanged so the picoCTF{...} wrapper survives.

    Learn more

    The Caesar cipher shifts every letter by a fixed offset, wrapping Z back to A. Encryption with shift s and decryption with shift -s (equivalently, encryption with shift 26 - s) are inverses: if the challenge encoded with +18, you decode with -18 (or +8). The script subtracts the shift, which is the decoding direction.

    Why the alphabetic-only branch? Digits, punctuation, braces, and underscores must pass through unchanged so the flag's structure (picoCTF{...}) survives. If you accidentally rotate digits too, the body of the flag becomes garbage even at the right shift.

    The keyspace is just 25 candidates. Brute force is not a workaround here, it is the optimal attack: at one millisecond per shift the entire space falls in 25 ms. Anything more sophisticated (frequency analysis, known-plaintext crib of picoCTF mapping to xqkwKBN) would be overkill and is mainly worth knowing for substitution ciphers with 26! candidate keys instead.

    For zero scripting, CyberChef has a ROT brute-force operation. The ROT13 Brute Force recipe tries all 25 shifts and prints the lot.

  3. Step 3
    Submit the flag
    Observation
    I noticed the brute-force output includes exactly one line beginning with 'picoCTF', which is the rigid flag format used by picoCTF and uniquely identifies the correct shift without any ambiguity.
    The single row that starts with picoCTF is the answer. Copy it without quotes.
    Learn more

    Exactly one shift produces a string starting with picoCTF because flag format is rigid: 7 specific letters in a specific order. Any wrong shift produces gibberish at those positions. That uniqueness is what makes brute-forcing classical ciphers safe in CTFs: there is exactly one plaintext-shaped output, and you cannot accidentally pick the wrong one.

Interactive tools
  • ROT / Caesar CipherDecode Caesar-shifted and ROT-encoded text. Drag the shift slider or scan all 26 rotations at once.
  • Cipher Identifier & Auto-DecoderPaste any ciphertext and the tool auto-runs every common decoder (base64, hex, Morse, ROT, Atbash, Bacon, binary, decimal, URL) and ranks the results by English-likeness.

Flag

Reveal flag

picoCTF{r0tat1o...d140864}

Any Caesar/ROT decoder works; the correct offset is 8 (the ciphertext was encoded with ROT8).

Key takeaway

Caesar and ROT ciphers have a keyspace of only 25 possible shifts, making brute force not just viable but optimal, because a known flag prefix like 'picoCTF{' pinpoints the correct plaintext in one pass. Classical substitution ciphers with larger keyspaces (26! for a general monoalphabetic substitution) fall to frequency analysis instead, exploiting the statistical distribution of letters in natural language. The same principle of exhaustive search over a tiny keyspace applies anywhere a secret is chosen from a small or predictable set, from PIN codes to short passwords.

Related reading

Want more picoCTF 2023 writeups?

Tools used in this challenge

What to try next