Onion: Cryptography

Flag: KPMG_CTF{p33l_th3_l4y3rs_l1k3_4n_0n10n}

File: chall.enc, 935 bytes of ASCII text, one line, no line terminators Category: Cryptography (layered encodings / classical cipher)

Scenario

Our old sysadmin loved stacking things - layer after layer, like he was trying to make you cry before you got to the middle. We found this on a decommissioned box. Peel it back.

“Layers” and “make you cry” point to an onion: the flag is wrapped in several encodings, one inside the other. The way to solve it is to look at each layer, work out what it is, decode it, and then do the same with the result until something shaped like KPMG_CTF{...} comes out.

Layers, outermost first:

binary (8-bit ASCII)  ->  Base64  ->  hex  ->  Atbash  ->  flag

Step 1: Triage

$ ls -la chall.enc && file chall.enc && head -c 300 chall.enc; echo; wc -c chall.enc
-rw-rw-r-- 1 user user 935 Oct 10 11:42 chall.enc
chall.enc: ASCII text, with very long lines (935), with no line terminators
01001110 01010100 01000001 00110000 01011001 01101010 01010010 01101100 01001110 01010100 ...
935

The file contains only 0, 1 and spaces, in groups of 8 bits. That is binary-encoded ASCII. Each group is 9 characters including its separator, so 935 characters ≈ 104 bytes.

Step 2: Layer 1: Binary → ASCII

Split on whitespace, convert each group from base 2 to a byte, and join the bytes:

$ python3 -c "
d=open('chall.enc').read().split()
print(bytes(int(b,2) for b in d))"
b'NTA0YjRlNTQ1ZjU4NDc1NTdiNmIzMzMzNmY1ZjY3NzMzMzVmNmYzNDYyMzM2OTY4NWY2ZjMxNzAzMzVmMzQ2ZDVmMzA2ZDMxMzA2ZDdk'

The result only uses the characters [A-Za-z0-9], and its length (104) is a multiple of 4, which points to Base64. It also starts with NTA0, which is what 504... becomes when Base64-encoded, so the next layer is probably hex.

Step 3: Layer 2: Base64 → hex string

$ echo 'NTA0YjRlNTQ1ZjU4NDc1NTdiNmIzMzMzNmY1ZjY3NzMzMzVmNmYzNDYyMzM2OTY4NWY2ZjMxNzAzMzVmMzQ2ZDVmMzA2ZDMxMzA2ZDdk' | base64 -d
504b4e545f5847557b6b33336f5f6773335f6f34623369685f6f3170335f346d5f306d31306d7d

Only 0-9a-f characters, even length: this is hex. The bytes 7b ({), 5f (_) and 7d (}) are visible, so a flag-shaped string is near.

Step 4: Layer 3: Hex → text

$ echo '504b4e545f5847557b6b33336f5f6773335f6f34623369685f6f3170335f346d5f306d31306d7d' | python3 -c "import sys;print(bytes.fromhex(sys.stdin.read().strip()).decode())"
PKNT_XGU{k33o_gs3_o4b3ih_o1p3_4m_0m10m}

(xxd -r -p does the same thing if xxd is installed.)

This has the shape of a flag (XXXX_XXX{...}), but the letters are scrambled. The digits and underscores are unchanged, so the last layer is a letter-only substitution cipher.

Step 5: Layer 4: Identifying the substitution

Attempt: Caesar / ROT-N (fails)

The first thing to try is all 26 shifts:

$ python3 -c "
h='PKNT_XGU{k33o_gs3_o4b3ih_o1p3_4m_0m10m}'
for k in range(26):
    print(k,''.join(chr((ord(c)-97+k)%26+97) if c.islower() else
                    chr((ord(c)-65+k)%26+65) if c.isupper() else c for c in h))"
0 PKNT_XGU{k33o_gs3_o4b3ih_o1p3_4m_0m10m}
...
13 CXAG_KTH{x33b_tf3_b4o3vu_b1c3_4z_0z10z}
...
21 KFIO_SBP{f33j_bn3_j4w3dc_j1k3_4h_0h10h}
...

None of the shifts give KPMG_CTF, so this is not a single-shift cipher.

Known-plaintext analysis → Atbash

We know the prefix must be KPMG_CTF. Line up ciphertext and plaintext letters:

Cipher P K N T X G U
Plain K P M G C T F

Two things stand out:

  • P → K and K → P: the mapping undoes itself (it is an involution).
  • Each pair is the same distance from the two ends of the alphabet: P(15) ↔ K(10), N(13) ↔ M(12), T(19) ↔ G(6), X(23) ↔ C(2), U(20) ↔ F(5); in every case a + b = 25.

That is the Atbash cipher: A↔Z, B↔Y, C↔X, …, i.e. plain = 25 − cipher (0-indexed). It has no key, and applying it twice gives back the original text.

Step 6: Layer 4: Atbash → flag

$ python3 -c "
h='PKNT_XGU{k33o_gs3_o4b3ih_o1p3_4m_0m10m}'
print(''.join(chr(219-ord(c)) if c.islower() else      # 'a'+'z' = 97+122 = 219
              chr(155-ord(c)) if c.isupper() else c   # 'A'+'Z' = 65+90  = 155
              for c in h))"
KPMG_CTF{p33l_th3_l4y3rs_l1k3_4n_0n10n}

The same thing with tr:

$ echo 'PKNT_XGU{k33o_gs3_o4b3ih_o1p3_4m_0m10m}' | tr 'A-Za-z' 'ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba'
KPMG_CTF{p33l_th3_l4y3rs_l1k3_4n_0n10n}

Leetspeak for “peel the layers like an onion”, which matches the challenge description.

One-shot solver

python3 -c "
import base64
s = bytes(int(b, 2) for b in open('chall.enc').read().split())   # binary -> ascii
s = base64.b64decode(s)                                           # base64
s = bytes.fromhex(s.decode()).decode()                            # hex
print(''.join(chr(219-ord(c)) if c.islower() else
              chr(155-ord(c)) if c.isupper() else c for c in s))  # atbash
"

Equivalent shell pipeline:

python3 -c "import sys;print(bytes(int(b,2) for b in open('chall.enc').read().split()).decode())" \
  | base64 -d | python3 -c "import sys;print(bytes.fromhex(sys.stdin.read().strip()).decode(),end='')" \
  | tr 'A-Za-z' 'ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba'; echo

CyberChef recipe: From Binary → From Base64 → From Hex → Atbash Cipher.

Takeaways

  • Identify each layer by its alphabet: {0,1,space} → binary; [A-Za-z0-9+/=] with length % 4 == 0 → Base64; [0-9a-f] with even length → hex.
  • Base64 of hex has a recognisable look: strings like NTA0, NjE, NzM, M2, Zj come from Base64-encoding ASCII hex digits.
  • Use the known flag prefix as known plaintext. When digits and punctuation survive but letters don’t, line up the scrambled prefix against KPMG_CTF. A mapping that undoes itself with a + b = 25 is Atbash; a constant difference is Caesar; a repeating pattern of differences is Vigenère.
  • Encoding (binary, Base64, hex) is not encryption. Only the last layer is a cipher, and Atbash has no key, so the whole onion provides no confidentiality.