Onion: Cryptography
On this page
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 → KandK → 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,Zjcome 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 witha + b = 25is 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.