Inshall'hack
Security if God wills it
Inshall'hack

Nice Code Writeup (ASIS CTF Quals 2018)

Nice Code was a Web challenge at the ASIS quals 2018. It was solved about 30 times, but contained a few nice tricks that are worth mentioning.

Challenge description

Beautify php code! Here (http://167.99.36.112:8080/)

Discovery and baby steps

We arrive on a page with a Get started button. Since it does not contain anything interesting, we just click on the button, and end up on a page whose URL is http://167.99.36.112:8080/admin/ that basically consists of the following string:

substr($URL, -10) !== '/index.php'

We simply add index.php to our URL, and stumble upon the following page:

$URL == '/admin/index.php'

Without any further information, we figured we would try different things. Adding a "?" at the end of the URL doesn't seem like the way to go, as it returns a page containing the following string:

empty($matches) || $matches[1] != $URL

After fiddling around with the URL, we found the correct path /admin/index.php/admin/index.php (still no clue how to do better than educated guessing here, though).

This leads us to a page that redirects us to the real challenge.

Time to hack

The new page is opened with the source parameter set. It contains the following code:

 <?php
include('oshit.php');
$g_s = ['admin','oloco'];
$__ni = $_POST['b'];
$_p = 1;
if(isset($_GET['source'])){
    highlight_file(__FILE__);
        exit;
}
if($__ni === $g_s & $__ni[0] != 'admin'){
    $__dgi = $_GET['x'];
    $__dfi = $_GET;
    foreach($__dfi as $_k_o => $_v){
        if($_k_o == $k_Jk){
            $f = 1;
        }
        if($f && strlen($__dgi)>17 && $_p == 3){
            $k_Jk($_v,$_k_o); //my shell :)
        }
        $_p++;
    }
}else{    
    echo "noob!";
}

Quite unreadable. Let's beautify it a bit:

<?php
include('oshit.php');
$credentials = ['admin','oloco'];
$_p = 1;

if($_POST['b'] === $credentials & $_POST['b'][0] != 'admin'){

    foreach($_GET as $key => $value){
        if($key == $shellfunc){
            $f = 1;
        }
        if($f && strlen($_GET['x']) > 17 && $_p == 3){
            $shellfunc($value, $key); //my shell :)
        }
        $_p++;
    }
}else{
    echo "noob!";
}

Much better! Checking the headers of the request, we notice that X-Powered-By is set to PHP/5.5.9-1ubuntu4.14. An old version of PHP! CTF challenges are a bit similar to drama, in the sense that the principle of Chekhov's gun applies more often than not. This will most likely come in handy later.

So, it seems that we won't be able to do anything without passing the first condition:

if($_POST['b'] === $credentials & $_POST['b'][0] != 'admin'){

The impossible condition

At first glance, the fact that the & operator is used instead of && seems weird. What does it change exactly? When both operands are booleans, & pretty much behaves like &&. A key difference is that no short circuit occur when using this operator, as opposed to &&. This means that both comparisons are done every time the line is hit. While we lost some time thinking about what that implies, it is utterly useless in this case, so for every purpose, we're working with the && operator.

Now, the condition seems impossible to satisfy: $_POST['b'] must be strictly equal to $credentials. Strict equality, in the context of PHP arrays, means that both arrays contain the same key-value pairs, with the same types and in the same order. This means that $_POST['b'][0] must absolutely be equal to $credentials[0] which is equal to "admin". This makes the whole condition seemingly impossible to satisfy. Well, at least theoretically. If it weren't breakable, there wouldn't be much of a challenge, would there?

Broken language

Once you know that this condition is theoretically impossible to pass, it is easy to deduce that something must be handled badly by PHP. The fact that PHP5 is used also hints towards that.

Even knowing that, it took us a long time to find the issue. After asking a friend, we were hinted towards one of Gynvael's vlogs. To sum it up, the strict equality operator is broken for arrays in PHP5; the reason is that the index used for comparison is a 32-bit int. This means that any integer index larger than 32 bits will be truncated. Once we know that, we just need to find an integer that would be truncated to 0. An obvious value for that is 232 = 4294967296.

Therefore, we can pass the first condition using the following POST data: b[4294967296]=admin&b[1]=oloco.

Not quite out of the woods yet

To finally get to the RCE, we need to pass a second condition:

if($f && strlen($_GET['x']) > 17 && $_p == 3){

While it contains three sub-conditions, two of them are fairly straightforward to satisfy: $_p == 3 is satisfied for our third $_GET parameter, meaning that our third parameter should contain the payload; strlen($_GET['x']) > 17 is satisfied if we add a parameter x with a value longer than 17 characters. Now, as for $f, it is a bit more complicated:

if($key == $shellfunc){
    $f = 1;
}

As we can see above, $f is only set if one of our $_GET parameters has a key equal to $shellfunc. $shellfunc is a variable that can be called. As such, it is either a variable function or an anonymous function (already available in PHP 5.5). Since the comparison is loose here, we can work some type juggling magic: $shellfunc == 0 will evaluated to true provided $shellfunc is a variable function; however, if it is an anonymous function, the key will have to be 1. It's impossible to decide which one is correct at this point.

Since $shellfunc uses $value as its first argument, it is most likely the command name, and $key the argument. With that in mind, we add the last one of our $_GET parameters: 100000=sleep, and the whole payload becomes: 0=yo&x=123456789012345678&100000=sleep. The request hangs, which means that $shellfunc was in fact a variable function.

Last sprint

After a few tries, we notice the shell actually executes PHP functions and not shell commands. However, we're able to execute system to get a shell. The last issue is that since the argument to system is the key of the parameter, it can not contain any space. We therefore need to find the flag without using any space.

This is quite straightforward, as the find command returns the paths to every possible file when used without an argument. We just grep flag in the output and discover the file /var/flag. cat can also be used to read the file without using spaces, like such: cat</var/flag!

And… Flagged!

Flag: ASIS{f52c5a0cf980887bdac6ccaebac0e8428bfb8b83}

Wrapping up

While program version-related bugs are less fun to find that their bad developer-induced counterparts, I enjoyed the challenge. I do feel a bit ashamed about the time it took me to solve it, but kudos to the organizers for making seemingly easy challenges quite complicated!


comments powered by Disqus

Receive Updates

ATOM

Contacts