Insomni'Hack 2018 - PHuck

CTF URL: https://insomnihack.ch/

Solves: 26 / Points: 120

Challenge description

A friend of me tell me that this script is a damn bad one in term of security. Can you explain me why?

Here is the PHP script:

<?php
	$dataset = [
		0 => ['Blaklis', 'The flag is INS{snip}.'],
		1 => ['Lambda guy', 'We don\'t have anything interesting to say'],
		2 => ['Lambda guy 2', 'We still do not say anything interesting'],
		3 => ['Lambda guy 3', 'PHP is the best language ever!']
	];


	$block = (function($request) {
		$blocked = FALSE;
		$keywords = ['_', 'admin=', '\'', '"', '[', ']', '\\', " ", chr(9),chr(10),chr(11),chr(12),chr(13),chr(133),chr(160),"%"];
		foreach($keywords as $keyword)
			if(strpos(urldecode($request),$keyword) !== FALSE)
				$blocked = TRUE;
		return ($_SERVER['REMOTE_ADDR'] === '127.0.0.1') ? FALSE : $blocked;
	})($_SERVER['REQUEST_URI']);
	!$block?:die('Die by the WAF!');
	
	if($_SERVER['REQUEST_METHOD'] === 'POST' && $_GET['is_admin'] == 1) {
		$data = str_replace(" ","",file_get_contents("php://input"));
		$datablock = (function($post_data) {
			$blocked = (strlen($post_data) > 30 || !($a = json_decode($post_data)));
			return $blocked;
		})($data);
		!$datablock?:die('Die by the WAF!');
		$a = (array)json_decode($data);

		if(isset($a['userid']) && ($a['userid'] != 0 || $_SERVER['REMOTE_ADDR'] === '127.0.0.1')) {
			if(isset($dataset[$a['userid']])) {
				echo "It's name is ".$dataset[$a['userid']][0]." and he would like to say : ".$dataset[$a['userid']][1];
				exit;
			}
		}
	}

	die('Die by... nothing?');

Challenge resolution

This challenge is not really complicated, but you need to know the dumb issues in PHP. So, to me, it was very frustrating!!

We can divide the PHP script in 2 parts.

Keyword filtering

At the beginning, the script ensures that there is no specific keywords in the URL ($_SERVER['REQUEST_URI']) such as _, % and admin=. The script is using strpos and urlencode functions. So we can’t use URL encoding to bypass the filter and since % character is filtered, we can’t use double URL encoding.

As you can see in the script, we need to have is_admin=1in the URL in order to go further but the filter is preventing that.

In order to bypass the admin= filter, we can use a NULL byte as follow: admin%00=.

But we are still stuck with the _ character. After many frustration and even thinking to find a way to fake my IP address to 127.0.0.1, I found the following PHP documentation:

So it appears that some characters such as (dot) or (space) are replace by _ (underscore) by PHP!!!!

For this reason, it is important to note that PHP will automatically replace any dots in incoming variable names with underscores. 

The full list of field-name characters that PHP converts to _ (underscore) is the following (not just dot):
chr(32) ( ) (space)
chr(46) (.) (dot)
chr(91) ([) (open square bracket)
chr(128) - chr(159) (various)

PHP irreversibly modifies field names containing these characters in an attempt to maintain compatibility with the deprecated register_globals feature.

JSON decoding and userid

Then, if we found a way to bypass the filter function, the script checks if the is_admin parameter is set to 1 and the request is using a POST method. After, there is another check to ensure that the POST data is a valid JSON and the length is not longer than 30 characters. The final check is to ensure that the userid variable is present in the JSON data and its value is different 0.

So the question is how to have a value different to 0 but then when used as an index is equal to 0?

After trying some PHP juggling, I noticed that we just needed to give a float as userid value. Indeed, if we use 0.009 for instance. When the script will compare it, it will different from 0 because it’s not a strict comparison (!=).

Putting All the pieces together

So at the end, we just need to send the following request, in order to obtain the flag!!!

Flag

Author: lopesg Guillaume Lopes | @Guillaume_Lopes

Post date: 2018-03-25