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
Walk me through it- Step 1Read the cipher from the filePath('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 2Brute-force all 25 shiftsCaesar 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}') PYLearn 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 3Submit the flagThe 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.
Flag
picoCTF{r0tat1o...d140864}
Any Caesar/ROT decoder works; the correct offset is 18.