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.
wget https://artifacts.picoctf.net/c/354/encrypted.txtcat encrypted.txtpython3 - <<'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}')
PYSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Read the cipher from the fileObservationI 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 thatread_textincludes, so the loop does not see\nas a non-alphabetic passthrough byte and waste a comparison.Step 2
Brute-force all 25 shiftsObservationI 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.pythonpython3 - <<'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}') PYExpected 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
picoCTFmapping toxqkwKBN) 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.
Step 3
Submit the flagObservationI 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
picoCTFbecause 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).