Skip to main content

rotation picoCTF 2023 Solution

Decrypt a flag encoded with a simple letter-shift cipher to recover the plaintext.

Published: April 26, 2023Updated: August 25, 2026

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{...}). Get the files from the challenge page on CyLab Security Academy (formerly play.picoctf.org).

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

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 1Read the cipher from the file
    Observation
    There is one file, encrypted.txt. Load it into a variable before trying 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 2Brute-force all 25 shifts
    Observation
    The name says rotation and the ciphertext is all shifted letters like 'xqkwKBN', so this is a Caesar cipher. The shift is the only unknown, and there are just 25 of them, so try them all.
    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

     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 is only correct when the shift really was 13. Applied to a ROT8 ciphertext it just lands on a different wrong plaintext. Nothing in the problem says which offset was used, so loop over all 25.

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

    Rotating digits breaks the flag: shift '0' by 8 and you get '8', so the body is wrong even at the correct letter shift. A Caesar cipher rotates letters only. Digits, braces, underscores, and punctuation pass through untouched 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 3Submit the flag
    Observation
    Exactly one line of the brute-force output starts with 'picoCTF'. The flag format is rigid, so that line names the correct shift with no 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

A Caesar cipher has a keyspace of 25, so brute force is not just viable but optimal, and a known prefix like 'picoCTF{' picks the right plaintext in one pass. Bigger substitution keyspaces fall to frequency analysis instead, using the letter distribution of natural language. Exhaustive search works anywhere a secret comes from a small or predictable set, from PIN codes to short passwords.

Related reading

Tools used in this challenge

Where to go next