MultiCode picoCTF 2026 Solution

Published: March 20, 2026

Description

We intercepted a suspiciously encoded message, but it's clearly hiding a flag. No encryption, just multiple layers of obfuscation. Can you peel back the layers and reveal the truth? Download message.txt.

Download message.txt and examine its contents.

Identify which encoding is the outermost layer.

bash
cat message.txt

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Download and inspect the message
    Observation
    I noticed the challenge description said 'multiple layers of obfuscation' with no encryption, which suggested the solution would require identifying and stripping each encoding layer in sequence, starting from the outermost one visible in message.txt.
    Download message.txt and look at the outermost encoding. The message ends with '=' signs and uses only A-Za-z0-9+/ characters, which is the base64 alphabet. The layers in order are: base64 -> hex charcode -> URL encoding -> ROT13.
    bash
    cat message.txt
    What didn't work first

    Tried: Trying to decode the raw message.txt output directly as hex, since CTF files often use hex encoding.

    The outer layer is base64, not hex. The presence of lowercase letters, uppercase letters, and the trailing '=' padding signs confirms base64. Hex-decoding a base64 string produces garbled binary output because the character sets overlap but are not identical - hex is a strict subset of the alphanumeric range while base64 uses the full A-Za-z0-9+/ set.

    Tried: Running 'file message.txt' or 'xxd message.txt' to detect a hidden binary format before decoding.

    The file command reports 'ASCII text' and xxd shows printable characters throughout, which gives no additional information beyond what 'cat' already shows. For pure encoding challenges the data is always printable text, so the identification step is about recognising the character set pattern rather than binary magic bytes.

    Learn more

    Encoding transforms data into a different representation without using a secret key. It is reversible by anyone who knows the encoding scheme - no key is required. This is fundamentally different from encryption, which requires a key to reverse. Common encodings include base64, hex, URL encoding, and binary representation. Stacking multiple encodings adds layers that must be peeled in reverse order, but provides no additional secrecy.

    The key to identifying the outermost layer is recognising the character set used. Base64 uses A-Za-z0-9+/=. Base32 uses A-Z2-7=. Hex uses 0-9a-fA-F with even length. Binary uses only 0 and 1 in groups of 8. Morse code uses ., -, spaces, and / as word separators. URL encoding uses %XX sequences. Recognising these patterns at a glance is a core CTF skill.

  2. Step 2
    Peel the four encoding layers in CyberChef
    Observation
    I noticed that after base64 decoding, the result was space-separated hex numbers, followed by percent-encoded sequences, and finally ROT13-shifted text, so each intermediate output pointed directly to the next decoding operation needed.
    Use CyberChef to peel each layer: (1) From Base64, (2) From Char Codes (base 16, space delimiter), (3) URL Decode, (4) ROT13. After all four operations the flag appears.
    python
    python3 << 'EOF'
    import base64, urllib.parse, codecs
    
    # Step 1: base64 decode
    step1 = base64.b64decode(open('message.txt').read().strip()).decode()
    # Step 2: from hex charcodes (space-separated hex numbers -> chars)
    step2 = ''.join(chr(int(x, 16)) for x in step1.split())
    # Step 3: URL decode
    step3 = urllib.parse.unquote(step2)
    # Step 4: ROT13
    step4 = codecs.decode(step3, 'rot_13')
    print(step4)
    EOF

    Expected output

    picoCTF{mult1c0d3_...}
    What didn't work first

    Tried: Using 'From Char Codes' in CyberChef with base 10 (decimal) instead of base 16 (hex) for the second layer.

    After base64 decoding, the intermediate data is space-separated hexadecimal numbers like '70 69 63 6f'. Treating them as decimal gives wrong character values - for example decimal 70 is 'F' but hex 70 is 'p'. The output looks like garbled punctuation rather than a readable string. The giveaway that the second layer is hex charcode is that all the numbers fall within the valid hex range 0-9a-fA-F and decode sensibly only when parsed as base 16.

    Tried: Applying the four CyberChef operations in reverse order (ROT13 first, then URL decode, then hex charcodes, then base64 last).

    Encoding layers must be peeled in the reverse of the order they were applied - outermost layer first, innermost last. Applying ROT13 first produces a garbled result because the data is still base64-encoded text, not ROT13-shifted text. The correct order is base64 -> hex charcodes -> URL decode -> ROT13, which mirrors peeling an onion from outside in.

    Learn more

    Each encoding has a reliable detection signature:

    • Base64: length divisible by 4 (with = padding), chars A-Za-z0-9+/=
    • Base32: uppercase letters and digits 2-7, = padding, length divisible by 8
    • Hex: even number of chars from 0-9a-fA-F
    • Binary: only 0 and 1, length divisible by 8
    • Octal: space-separated groups of 0-7 digits
    • Decimal ASCII: space-separated integers in range 32-126
    • URL encoding: contains % followed by two hex digits
    • Morse code: only ., -, spaces, and /
    • ROT13: looks like English text but shifted; applying ROT13 again reveals the original

    CyberChef(gchq.github.io/CyberChef/) is an invaluable tool for multi-layer encoding challenges. Its "Magic" operation automatically detects and applies decoding operations, and you can chain operations visually to peel layers one by one. For automated scripting, the Python script in the next step handles all these cases programmatically.

  3. Step 3
    Auto-peel all layers with a script
    Observation
    I noticed that manually applying four operations in CyberChef works for a fixed layer order but would break if the server randomized the order or added extra layers, which suggested writing a greedy auto-detection script that identifies each encoding from its character set and strips it programmatically.
    Use this script to automatically detect and peel every encoding layer in sequence until the flag is revealed.
    python
    python3 - <<'EOF'
    import base64, re, urllib.parse
    
    MORSE = {'.-':'A','-...':'B','-.-.':'C','-..':'D','.':'E','..-.':'F',
      '--.':'G','....':'H','..':'I','.---':'J','-.-':'K','.-..':'L',
      '--':'M','-.':'N','---':'O','.--.':'P','--.-':'Q','.-.':'R',
      '...':'S','-':'T','..-':'U','...-':'V','.--':'W','-..-':'X',
      '-.--':'Y','--..':'Z','-----':'0','.----':'1','..---':'2',
      '...--':'3','....-':'4','.....':'5','-....':'6','--...':'7',
      '---..':'8','----.':'9'}
    
    def peel(s):
        # Encoding-precedence order (most-restrictive char set first):
        # binary -> morse -> octal -> decimal -> hex -> URL -> base32 -> base64 -> rot13
        # This order is irrecoverable from reading the script alone, so it's
        # documented here. Reorder and you'll mis-classify (e.g. binary as hex).
        s = s.strip()
        c = re.sub(r'[\s:0x\\]', '', s)
        # binary
        cb = s.replace(' ','').replace(',','')
        if re.fullmatch(r'[01]+', cb) and len(cb)%8==0:
            try: t=''.join(chr(int(cb[i:i+8],2)) for i in range(0,len(cb),8)); return 'binary',t
            except: pass
        # morse
        if re.fullmatch(r'[./- \t\n]+', s) and ('.' in s or '-' in s):
            try:
                words=[' '.join(MORSE.get(l,'?') for l in w.strip().split()) for w in s.split('/')]
                return 'morse',' '.join(words)
            except: pass
        # octal
        parts=re.split(r'[\s,]+', s)
        if len(parts)>3 and all(re.fullmatch(r'[0-7]{1,3}',p) for p in parts):
            try: return 'octal',''.join(chr(int(p,8)) for p in parts)
            except: pass
        # decimal ASCII
        try:
            nums=[int(p) for p in parts]
            if len(nums)>3 and all(32<=n<=126 for n in nums):
                return 'decimal',''.join(chr(n) for n in nums)
        except: pass
        # hex
        if re.fullmatch(r'[0-9a-fA-F]+', c) and len(c)%2==0:
            try: return 'hex', bytes.fromhex(c).decode()
            except: pass
        # URL
        if '%' in s:
            t=urllib.parse.unquote(s)
            if t!=s: return 'url', t
        # base32: pad to multiple of 8 dynamically
        try:
            b32_padded = s.upper() + '=' * ((8 - len(s) % 8) % 8)
            t = base64.b32decode(b32_padded).decode()
            # isprintable() is too permissive (matches all-whitespace garbage);
            # require at least one alphanumeric to kill mis-padded false positives.
            if any(ch.isalnum() for ch in t): return 'base32', t
        except: pass
        # base64: pad to multiple of 4 dynamically
        try:
            b64_padded = s + '=' * ((4 - len(s) % 4) % 4)
            t = base64.b64decode(b64_padded).decode()
            if any(ch.isalnum() for ch in t): return 'base64', t
        except: pass
        # rot13
        t=s.translate(str.maketrans(
            'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
            'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'))
        if 'pico' in t.lower(): return 'rot13', t
        return None, s
    
    data = open('message.txt').read().strip()
    for i in range(20):
        if m:=re.search(r'picoCTF[{][^}]+[}]', data): print('FLAG:', m.group()); break
        name, data = peel(data)
        if name: print(f'[{i+1}] {name}: {data[:80]}')
        else: print('No decoder matched'); break
    EOF
    What didn't work first

    Tried: Running the auto-peel script but it exits with 'No decoder matched' after the URL decode step instead of finding the flag.

    The script checks for the flag pattern with 'picoCTF' (capital C, T, F) but ROT13-encoded text contains 'cvpbPGS' at that point. The final ROT13 layer is only applied if the script actually matches and peels it - if the ROT13 branch requires 'pico' to already be visible in the shifted output to accept the decode, then the order of the rot13 guard condition matters. The script uses 'if pico in t.lower()' as a heuristic, which only fires after rot13 produces readable plaintext, so the issue is usually a loop iteration limit too low for the number of layers.

    Tried: Hardcoding the four-step Python decode (base64 -> hex charcodes -> URL -> ROT13) from step 2 instead of running the greedy auto-peeler when the actual challenge has different or more layers.

    The manual four-step script only works for this specific instance of the challenge where the layer order is known in advance. If the competition server uses a different layer ordering or adds extra layers, the hardcoded script silently produces wrong output with no error. The greedy auto-peeler is more robust because it detects each layer from the data itself rather than assuming a fixed order.

    Learn more

    The auto-peeling script implements a greedy decoder: at each step it tries all known encoding schemes in order and applies the first one that succeeds, then repeats on the result. The order matters - binary detection (only 0s and 1s) must come before hex (which is a superset of binary characters); Morse must come before decimal (both use only certain characters). The script terminates when the flag pattern picoCTF{...} is found or when no decoder matches.

    Building a robust multi-encoding detector requires handling edge cases: base64 padding (== or = appended), base32 requiring uppercase and padding to a multiple of 8, hex strings that are also valid base64, and Morse code that contains spaces (word separator) and slashes (letter separator). The script handles these by trying the most specific matchers first and falling back gracefully.

    This type of challenge teaches encoding literacy: the ability to recognise data representations at a glance. In real incident response and malware analysis, attackers commonly encode payloads in base64 (to bypass text-based filters), hex-encode shellcode (for embedding in source code), and use multiple encoding layers to evade signature-based detection. Being able to peel these layers quickly is an essential analyst skill. See the encodings guide for the full reference.

Interactive tools
  • Base64 & Base32 DecoderDecode Base64 and Base32 strings with auto-detection. Multi-layer mode unwraps nested encodings automatically.
  • Recipe ChainStack decoders into a pipeline: Base64, hex, ROT, XOR, Morse, URL, Atbash, Vigenère, and more. Magic mode auto-discovers the chain. Bookmark the URL to save it.
  • Number Base ConverterConvert numbers between binary, octal, decimal, and hexadecimal instantly. Enter any value and see all four bases update in real time.
Alternate Solution

If you want to decode individual layers by hand, the site has tools for each encoding you will encounter: the Base64 Decoder, Morse Code Decoder, ROT / Caesar Cipher tool, Number Base Converter, and Binary → Hex Converter. Work layer by layer, pasting the output of each tool into the next, until the flag appears.

Flag

Reveal flag

picoCTF{mult1c0d3_...}

Four stacked layers: base64 -> hex charcode (space-separated) -> URL encoding -> ROT13. Use CyberChef with those four operations in order to reveal the flag.

Key takeaway

Encoding schemes like base64, hex, and URL encoding transform data into a different representation without any secret key, so they provide zero confidentiality; anyone who recognizes the format can reverse it immediately. Stacking multiple encoding layers adds apparent complexity but no actual security, because each layer is still publicly reversible. In malware analysis and incident response, multi-layer encoding is a common obfuscation tactic used to evade signature-based detection, and being able to recognize each scheme by its character set and strip the layers programmatically is an essential analyst skill.

Related reading

Want more picoCTF 2026 writeups?

Useful tools for General Skills

What to try next