DEV Community

CTFDojo
CTFDojo

Posted on Originally published at ctfdojo.com

PicoCTF Mod 26 Writeup — Brute-Force a Caesar Cipher

The Caesar cipher has exactly 26 possible shifts (modulo the 26 letters of the alphabet). A Python script that tests all 26 shifts and looks for picoCTF in the result finds the right one in a fraction of a second.

  • Platform: picoGym
  • Category: Cryptography
  • Points: 100 pts
  • Difficulty: Beginner
  • Technique: Caesar cipher, brute force

Challenge description

The challenge provides a ciphertext and a hint that clearly points the way:

"I made my really long password more secure by adding 26 to each letter... modulo the number of letters."

tmgsgxj{________________________}
Enter fullscreen mode Exit fullscreen mode

The word "modulo" combined with text that looks like letters shuffled in a regular pattern is a strong hint toward a Caesar cipher.

Step 1 — Understand the Caesar cipher

The Caesar cipher is one of the oldest known ciphers: each letter of the plaintext is replaced by the letter located n positions further in the alphabet, where n is the shift (the key). For example, with a shift of 3: A→D, B→E, C→F, etc. Once we reach Z, we wrap back around to A — hence the "modulo 26" in the hint, since the Latin alphabet has 26 letters.

Mathematically, for a letter at position p in the alphabet (A=0, B=1, ..., Z=25) and a shift k:

encryption: c = (p + k) mod 26
decryption: p = (c - k) mod 26
Enter fullscreen mode Exit fullscreen mode

Step 2 — Brute force across the 26 shifts

Caesar's fundamental weakness is that the key space is ridiculously small: there are only 26 possible shifts (0 to 25), one of which does nothing (shift 0) and one of which is the famous ROT13 (shift 13). No need to guess anything: we test all 26 in a loop and see which one produces readable text.

def caesar_decrypt(text, shift):
    result = []
    for ch in text:
        if ch.isalpha():
            base = ord('A') if ch.isupper() else ord('a')
            decoded = chr((ord(ch) - base - shift) % 26 + base)
            result.append(decoded)
        else:
            result.append(ch)  # leave digits, punctuation, spaces untouched
    return ''.join(result)

ciphertext = "tmgsgxj{________________________}"

for shift in range(26):
    print(f"shift {shift:2d} : {caesar_decrypt(ciphertext, shift)}")
Enter fullscreen mode Exit fullscreen mode

Step 3 — Identify the right shift

Running the script gives us 26 lines of output. Most are gibberish, but one jumps right out — the one starting with picoCTF{:

shift  0 : tmgsgxj{________________________}
shift  1 : slfrfwi{...}
...
shift  3 : qjdpduf{...}
shift  4 : picoctf{________________________}
shift  5 : ohbnbse{...}
...
shift 13 : ghtdtjy{...}
...
Enter fullscreen mode Exit fullscreen mode

Shift 4 gives perfectly readable text, with the expected prefix picoctf{ — that's our shift. In practice, you just scan the 26 lines by eye, or script the detection by automatically searching for the substring picoctf{ in each output.

🚩 picoCTF{ flag intentionally hidden }

The flag is deliberately hidden — follow the method, you've earned it. 💪

Key takeaways

Caesar is the textbook example of a cipher "broken by design": its security relies entirely on the secrecy of the method, not on mathematical robustness. As soon as you know it's Caesar, the key is found instantly.

  • The Caesar cipher is cryptographically weak because its key space is ridiculously small (26) — always consider brute force first on such a small key space
  • Searching for a known pattern (here picoCTF{) across the 26 outputs lets you fully automate detecting the right shift, with no manual reading
  • This principle generalizes to ROT13 and any substitution variant with a fixed shift — the number of keys is always bounded by the size of the alphabet

Originally published on CTFdojo — join the CTFdojo Discord to discuss writeups and get notified about new ones.

Top comments (0)