Skip to main content

unpackme.py picoCTF 2022 Solution

The script decrypts and execs its real body, so print that body instead and the source reveals password and flag.

Published: July 20, 2023Updated: August 25, 2026

Description

The provided Python script base64-decodes an embedded payload and execs it. Intercept the decoded string to review the logic and recover the flag/password without blindly executing unknown code.

Download unpackme.flag.py and open it in your editor.

Before the exec(plain.decode()) line, insert a print(plain.decode()) (or store the decoded string) to view what will execute.

Wrap exec in try/except so a malformed payload doesn't kill the process silently. If the decode path itself fails, fall back to manually constructing a Fernet object with the embedded key and calling decrypt() on the base64 payload.

Run the script locally to print the hidden password and flag.

bash
wget https://artifacts.picoctf.net/c/48/unpackme.flag.py
bash
less unpackme.flag.py
python
python3 unpackme.flag.py
python
python3 - <<'PY'
# Defensive variant: print the payload, fall back to manual decode on error.
import base64
src = open('unpackme.flag.py').read()
try:
    g = {'__name__': '__main__'}
    exec(src, g)  # original loader runs print(); replace exec(plain) with print(plain) inside the file first
except Exception as e:
    print('Loader failed:', e)
    # Manual fallback: extract the base64 payload and key string from the source by eye, then:
    # from cryptography.fernet import Fernet
    # import base64
    # key = base64.urlsafe_b64encode(b'correctstaplecorrectstaplecorrec')
    # plain = Fernet(key).decrypt(B64_PAYLOAD)
    # print(plain.decode())
PY

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Inspect the decoder
    Observation
    The script decrypts with Fernet using a hardcoded key, then calls exec() on the result. Swap that exec for a print and the hidden payload is exposed without running unknown code.
    The script imports Fernet from the cryptography library, uses the hardcoded key string correctstaplecorrectstaplecorrec (base64-encoded) to construct a Fernet object, decrypts the embedded payload, and finally calls exec on the decrypted source. Printing plain.decode() reveals the cleartext code.
    What didn't work first

    Tried: Running the original script unmodified to see what it outputs.

    Unmodified, the script hands the decrypted payload straight to exec, which may prompt for a password or do other things without ever printing the flag. Intercept before that: replace the exec call with a print, so the hidden source is displayed rather than silently run.

    Tried: Trying to base64-decode the embedded payload manually without using the Fernet key.

    The payload is Fernet-encrypted, not merely base64-encoded, so a raw base64 decode returns binary ciphertext rather than Python source. Decryption needs both the key and the right library call, using the hardcoded key already sitting in the script.

    Learn more

    Self-modifying or self-decoding scripts use a two-stage approach: the outer script contains an encoded payload and decryption logic; when run, it decodes the payload and executes it dynamically. Python's exec() function runs a string as Python code, making this pattern possible in a single file.

    This technique is widely used in malware obfuscation: malicious scripts are encoded (Base64, XOR, zlib, etc.) to evade signature-based antivirus detection. The outer loader decodes and executes the real payload in memory. Multiple layers of encoding are common - each layer decodes the next. Security analysts "unpack" each layer by intercepting the decoded payload before execution, exactly as this challenge demonstrates.

    The safe approach is to replace exec(payload) with print(payload) - this reveals what the script would execute without actually running it. In a sandboxed environment, you can run the original safely, but in real incident response, you always analyze before executing unknown code.

    When PyCDC or uncompyle6 actually matter. They are only needed if the challenge ships compiled bytecode (a .pyc file or a frozen executable like PyInstaller output) without the matching source. This particular challenge ships plain Python source with a Fernet-encrypted payload inside, so neither tool is required: the "decompile" step is just printing the decrypted string. Save PyCDC/uncompyle6 for cases where you only have .pyc bytecode, and use Python's built-in dis module if you want to disassemble code objects you produced with compile().

  2. Step 2Recover the credentials
    Observation
    With exec replaced, the decrypted payload prints a password prompt alongside plaintext output. The hidden source carries both the password and the flag in what it prints.
    Executing the modified file prints a message containing both the password (batteryhorse) and flag. Revert to the original script if desired and supply the password to reproduce the flag output.
    What didn't work first

    Tried: Looking for the flag directly in the original .py source file by searching for 'picoCTF'.

    The flag is inside the encrypted Fernet payload, not visible in the outer script source. A text search or strings command on the file will only find the base64-encoded ciphertext, which looks like random characters. The flag only becomes readable after decryption.

    Tried: Installing pycdc or uncompyle6 to decompile the script, expecting a .pyc bytecode file.

    The challenge ships plain Python source (.py), not compiled bytecode (.pyc). Decompilers like pycdc and uncompyle6 operate on bytecode and have no effect here. The correct approach is to intercept the decrypted string at runtime by swapping exec() for print().

    Learn more

    Fernet is a symmetric authenticated encryption scheme built on AES-128-CBC with HMAC-SHA256. It is designed so that decryption fails loudly if the ciphertext has been tampered with. Because the key is hardcoded in the same script as the payload, it provides obfuscation rather than real security - anyone who reads the source can extract the key and decrypt immediately, exactly as this challenge demonstrates.

    Dynamic analysis - running code in a controlled environment and observing behavior - often reveals secrets that static analysis misses. For obfuscated malware, analysts run samples in sandboxes (like Any.run, Cuckoo Sandbox, or CAPE Sandbox) and capture: network connections, file system changes, registry modifications, and spawned processes. Replacing exec with print is the manual equivalent.

    Python's compile() and dis.dis() provide another approach: compile the string to a code object and disassemble it to bytecode, which is readable without execution. This is safer for analyzing potentially dangerous payloads and is the foundation of Python reverse engineering tools.

Interactive tools
  • File Magic IdentifierIdentify file types from magic numbers. Paste hex bytes or drop a file to detect PNG, JPEG, ZIP, PDF, ELF, PCAP, SQLite, and dozens of other formats.
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{175_chr157m45_5274...}

Always inspect self-modifying scripts before running them; a simple print statement exposes the payload safely.

Key takeaway

Script obfuscation puts an encoding or encryption step in front of the real payload to beat static signature scanning, then reaches for the language's eval primitive, exec in Python, eval in JavaScript, IEX in PowerShell, to run the decoded code. The insight is that a self-decoding script must produce cleartext before it can execute it, so intercepting at that moment, by turning the eval into a print, reveals the code without running it. Malware authors use the technique to evade antivirus, and analysts use the same interception to reverse them.

Related reading

Useful tools for Reverse Engineering

Where to go next