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
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
    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
    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
    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
    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.

Flag

picoCTF{r0tat1o...d140864}

Any Caesar/ROT decoder works; the correct offset is 18.

Want more picoCTF 2023 writeups?

Tools used in this challenge

Related reading

What to try next