Skip to main content

Apriti sesamo picoCTF 2025 Solution

Recover the leaked backup source, then pass arrays so PHP's loose comparison accepts the check without a password.

Published: April 2, 2025Updated: August 25, 2026

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.

Web

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:<PORT_FROM_INSTANCE>/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 1Recover the backup
    Observation
    The challenge dangles an impossible login and hidden PHP source. Editors leave backup files behind, so try the filename with a tilde appended.
    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 2Exploit PHP type juggling with arrays
    Observation
    The recovered source compares sha1($username) === sha1($pwd) with strict equality. If both calls return null the check passes, and PHP's sha1() returns exactly that when handed an array instead of a string.
    Use Burp Suite to intercept the login POST request. Change the body so that both username and pwd are arrays: username[]=a&pwd[]=b. On the PHP 7 runtime this challenge serves, sha1() of an array emits a warning and returns null. 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:<PORT_FROM_INSTANCE>/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 ==

    This code uses strict equality, not loose. Magic hashes only work against == where PHP coerces both sides to zero. With ===, types are compared too, so two 0e strings match only if they are identical, which cannot happen when the usernames differ. The array trick works because null equals null strictly, whatever the inputs were.

    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 SHA1 fixed point is known, and even one would not help: the server hashes each input separately, so a string equal to its own hash still fails the gate requiring the two inputs to differ. The array approach makes both calls return null at once, needing no cryptographic property at all.

    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 on PHP 7 and earlier. (PHP 8 tightened this: internal functions now throw a TypeError instead, which is why this exact bypass only lands on older runtimes like the one behind this challenge.) 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

Type juggling lives in the gap between what a function expects and what it actually does with an unexpected type. Build a security check around sha1() or md5() and an array argument quietly yields null, making two unrelated inputs compare equal. Any dynamically typed language with implicit coercion has this problem, and the fix is always to enforce the expected type before a security-sensitive comparison.

Related reading

Tools used in this challenge

Where to go next