Apriti sesamo picoCTF 2025 Solution

Published: April 2, 2025

Description

ABC Bank's "impossible" login hides a PHP backup. The source shows it hashes username and password with SHA1 and returns the flag when the hashes match but the raw values differ. PHP's sha1() returns null (not a string) when passed an array, and null === null, so passing arrays for both fields bypasses the check.

Append ~ to impossibleLogin.php (Emacs backup convention) to recover the actual PHP source.

Decode the Base64 constants in the source to learn the POST parameter names (username, pwd) and the SHA1-equality check.

Use Burp Suite (or curl) to intercept the login POST and change username and pwd from strings to arrays.

bash
curl -o login.php.bak http://verbal-sleep.picoctf.net:50313/impossibleLogin.php~
bash
# Intercept the login POST with Burp Suite and change the body to:
bash
# username[]=a&pwd[]=b

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Recover the backup
    Observation
    I noticed the challenge hinted at an 'impossible' login with hidden PHP source, which suggested the server-side code might be recoverable via an editor backup artifact such as an Emacs tilde file appended to the PHP filename.
    Emacs creates backup files by appending a tilde to the filename. Browse impossibleLogin.php~ to download the real PHP source. Decode the Base64 constants inside: they reveal the POST field names username and pwd, and the comparison sha1($username) === sha1($pwd) && $username !== $pwd.
    Learn more

    Emacs editor backup files are created automatically when Emacs opens a file for editing. The backup gets the same filename with a trailing tilde (~). When developers edit server-side files directly using Emacs in a web-accessible directory and forget to delete the backups, they become publicly downloadable - exposing source code that was never meant to be served.

    Common variants include Vim swap files (.swp, .swo), nano backup files (.save), and .bak or .old suffixes left by IDEs. Web servers do not strip these by default, so they must be explicitly blocked in server configuration.

    The Base64 encoding inside the PHP source is a thin attempt to obscure hardcoded constants. Once the source is obtained, decoding takes a single command. Encoding rather than encrypting gives only the illusion of security.

  2. Step 2
    Exploit PHP type juggling with arrays
    Observation
    I noticed the decoded PHP source used sha1($username) === sha1($pwd) with a strict equality check, which suggested that if sha1() could be made to return null for both inputs simultaneously, the check would trivially pass; PHP does exactly this when arrays are passed instead of strings.
    Use Burp Suite to intercept the login POST request. Change the body so that both username and pwd are arrays: username[]=a&pwd[]=b. In PHP, sha1() of an array returns null (with a warning). Since null === null, the hashes match. The raw values differ (both are arrays but were passed different dummy strings), so the != check passes too. The server returns the flag.
    bash
    # 1. Open Burp Suite > turn on Intercept > log in with any username/password
    bash
    # 2. In the intercepted request change:
    bash
    #    username=a&pwd=b
    bash
    # to:
    bash
    #    username[]=a&pwd[]=b
    bash
    # 3. Forward the request
    bash
    bash
    # Or with curl directly:
    bash
    curl -X POST -d 'username[]=a&pwd[]=b' http://verbal-sleep.picoctf.net:50313/impossibleLogin.php

    Expected output

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

    Tried: Trying to break the check with loose equality magic hashes - e.g. supplying '0e...' style strings that PHP coerces to zero under ==

    The code uses strict equality (===), not loose equality (==). Magic hash strings only work against loose == comparisons where PHP coerces both sides to the integer 0. Under ===, PHP also checks type, so two string '0e...' values would only match if they are the exact same string - they will never satisfy sha1($username) === sha1($pwd) when $username !== $pwd as strings. The array-to-null technique works here precisely because === null === null is true regardless of string content.

    Tried: Sending the curl request with a regular string that is already the SHA1 of itself (a fixed-point), hoping sha1(sha1(x)) === sha1(x) creates a collision

    No known SHA1 fixed point exists where sha1(x) === x as a raw string, and even if it did, both sides of the check receive the original input value, not its hash. The server computes sha1($username) and sha1($pwd) separately, so finding a string equal to its own hash would only produce sha1(same) === sha1(same), which still fails the $username !== $pwd gate. The array approach short-circuits both sha1() calls to null simultaneously, bypassing the need for any cryptographic property.

    Learn more

    PHP type juggling arises from PHP's loose type system. Many built-in functions accept any type and silently convert or return a fallback when the input is unexpected. Passing an array to sha1() triggers a warning and returns null. When the code does sha1($username) === sha1($pwd) and both calls return null, the strict equality === check passes - null equals null. At the same time, the arrays themselves are not equal as values, so the !== check between the raw inputs also passes. The login condition is satisfied without ever providing matching credentials.

    This technique is well-known in PHP security research. Real-world variants include comparing MD5 hashes of arrays (same behavior), using loose equality == with certain hash strings that PHP coerces to zero (magic hash / type confusion), and passing NULL or booleans to functions expecting strings. The underlying root cause is that PHP's type system was designed for convenience, not security, and functions that accept any type can produce unexpected results in a security-sensitive context.

    Burp Suite is the standard web proxy for intercepting and modifying HTTP traffic. The Proxy tab captures requests before they leave the browser, the Repeater tab lets you resend a modified request any number of times, and the Decoder tab converts between Base64, URL encoding, and other formats. See the Burp Suite for picoCTF guide for the full setup workflow.

Interactive tools
  • Reverse Shell GeneratorGenerate reverse shell payloads (bash, nc, python, perl, ruby, php, node, powershell) and matching listeners. Set host and port once, copy any variant.

Flag

Reveal flag

picoCTF{w3Ll_d3sErV3d_Ch4mp_5b26...}

Passing arrays instead of strings causes sha1() to return null for both, making null === null evaluate to true.

Key takeaway

PHP type juggling exploits the gap between what a function is designed to receive and what it actually does with unexpected input types. When a security check is built around the output of a function like sha1() or md5(), passing an array instead of a string can silently produce null, causing two unrelated inputs to compare as equal. The same class of vulnerability appears in any dynamically typed language where type coercion is implicit, and the fix is always to validate and enforce the expected type before any security-sensitive comparison.

Related reading

Want more picoCTF 2025 writeups?

Tools used in this challenge

What to try next