WEBVERSE

Loading...

mediumSQL InjectionPro

Almanac

Almanac is a private journaling app — write dated entries, keep your photos, and carry the whole archive between devices. The catch is the "carry it across" part: exporting your capsule and importing it on another device are two halves of the same trusting handshake.

The Scenario

Almanac is a three-person studio out of Galway that has been keeping people's journals since 2019. Their whole pitch is "your years, kept" — no lock-in, export to a single portable .capsule file anytime, restore it on a new phone in under a minute. To make moving devices effortless, the import feature reconstructs your archive object straight from the uploaded file and shows you its title and entry count before merging. It reconstructs that object a little too faithfully.

Challenge Intel

Synopsis

The "Import an archive" feature reconstructs the uploaded capsule with a naive pickle.loads() and renders the result's title back to the user. A crafted capsule whose title is a __reduce__ gadget runs code on the server and renders the flag in-band as the imported capsule's title.

What It Is

Almanac's Export produces base64(pickle.dumps(capsule_dict)) where capsule_dict is {"title", "created", "entries"}. The Import route (/import) does base64.b64decode() then pickle.loads() on the supplied blob with no validation, type-checking, or safe-loader — the canonical Python insecure-deserialization sink. The player recognises the format from their OWN export (the base64 decodes to a byte stream beginning with the 0x80 pickle-protocol opcode; the UI never names "pickle"), then crafts a malicious capsule. The intended payload is a dict whose "title" value is an object with a __reduce__ method returning (eval, ("__import__('subprocess').check_output(['cat','/flag.txt']).decode().strip()",)). On pickle.loads, __reduce__ executes: eval runs the command, and the object is reconstructed AS the string the command returned — i.e. the flag. The app's normal confirmation line, "Imported capsule: '<title>'", then prints that flag verbatim, in-band. Jinja2 autoescape stays on; a flag has no HTML metacharacters, so the player can at most XSS their own session — a non-issue. The flag is at /flag.txt (chmod 644, written by entrypoint) and is reachable ONLY through this code execution: there is no /flag route, no debug endpoint, and no page that prints it.

Who It's For

Players comfortable with the idea that deserializing attacker-controlled data can execute code. Assumes basic Python and the ability to run a short local script to build the payload. The twist over a trivial RCE is recognising the export's binary format from first principles and using a clean-rendering gadget so the flag arrives as readable text, not a crash.

Skills You'll Practice

  • Recognising a base64'd Python pickle stream from its 0x80 opcode
  • Writing a __reduce__ gadget that returns a command-execution callable
  • Choosing eval(...) so the reconstructed object IS the clean output string
  • Driving an import/restore feature as an insecure-deserialization sink

What You'll Gain

  • pickle.loads on untrusted input is remote code execution, full stop
  • Import/restore and 'portable backup' features are classic deserialization sinks
  • Output encoding (autoescape) does not mitigate a deserialization RCE
  • Never deserialize a format the user can hand-craft without a strict allowlist

Ready to hack Almanac?

Upgrade to Pro to unlock this challenge and the full library.