It was nice teaming up with other teams. We made 6th place at the 0CTF/TCTF 2020 Quals and split up who writes up which chall. Here's my writeup.
But be sure to also check out Robin Jadoul's writeup for PyAuCalc!
Oops, it looks like we got an unintended solution there, - Robin Jadoul
A misc challenge that has nothing to do with the cloud, nor with computing.
Welcome to our new cloud function computing platform, enjoy here. http://pwnable.org:47780/
With those quoted words as the only description, a look at the website kindly gives us some of its source code:
x<?php
error_reporting(0);
include 'function.php';
$dir = 'sandbox/' . sha1($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch ($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
if (waf($data)) {
die('waf sucks...');
}
file_put_contents("$dir" . "index.php", $data);
case 'shell':
initShellEnv($dir);
include $dir . "index.php";
break;
default:
highlight_file(__FILE__);
break;
}
Considering those options one at a time:
?action=pwd
prints a hash that defines the name of our personal sandbox dir. Why sandbox? Because we can only act within that dir, as we will see later.?action=shell
Does something we don't know and then includes index.php
from said directory. So it would be nice if we could specify what is in index.php
and then have it executed like this.?action=upload
Accepts some data and if waf doesn't suck it writes the data to index.php
.
Since there is no break
statement, it will also proceed with the shell
action.Wonderful. What is waf? Your favourite search engine explains that it stands for Web Application Firewall
. Sounds like it sucks, tbh.
As hyperreality summarised it:
So we upload a php file, it gets filtered by a WAF which we can't see, and then we can execute it?
We figured out what it does not like pretty quickly:
$_GET
"$()/;<=>?[\]{}~
all allowed, other ascii symbols banned - hyperreality [cr0wn]
We also noticed that some functions are disabled.
I don't think scandir works. neither does exec, system or passthru. - Sansero [flagbot]
Let's make those things work. That was not a fruitful approach, but we did nonetheless consider some options.
In order to work around the character count limit, it would be nice if we could include a file from our own domain. For that, we'd need a short enough domain. But wait! domains contain dots!
To work around that problem, we can specify the IP address without dots, as a decimal number.
Or we could also store the actual payload in a different GET parameter and eval that. But we don't have underscores. Since we can use eval though, we can use the character code.
<?eval("echo($\x5f\x47\x45\x54);");
prints Array, eval seems to work. - bazumo [flagbot]
At this point, bazumo made an important discovery: if we specify in the GET request data
as an array - i.e. ?data[]=print("HELLO");
instead of ?data=print("HELLO");
- waf does not complain!
that's sick, we can now get arbitrary length payloads - hyperreality [cr0wn]
wrong.
Request-URI Too Long The requested URL's length exceeds the capacity limit for this server. Apache/2.4.38 (Debian) Server at 172.23.0.2 Port 8000
pasting a complete urlencoded c99 shell is too much.
- LucidBrot [flagbot]
With get_defined_functions(true)
we get a list of only the enabled functions.
I soon came to believe that readfile(index.php)
did not work despite being on that list whereas highlight_file(index.php)
worked. In retrospect, that issue was probably just my browser which decided to put the output of readfile
into a comment instead of printing it on the page, because I hadn't wrapped the call in print()
.
First of all, we tried to activate all error_reporting.
xxxxxxxxxx
error_reporting ([ int$level ] ) : int
The error_reporting() function sets the error_reporting directive at runtime. PHP has many levels of errors, using this function sets that level for the duration (runtime) of your script. If the optional
level
is not set, error_reporting() will just return the current error reporting level.
The documentation sounds straightforward enough. But it is not, unless one reads closely. $level
is a bitmask, so we actually needed to call error_reporting(-1);
, which I find really counterintuitive.
Anyway, with error reporting enabled, we have something to work with:
Warning: readfile(): open_basedir restriction in effect. File(/var/www/html/sandbox/index.php) is not within the allowed path(s): (/var/www/html/sandbox/1c8606cc4b48bc4fc247f31cdd94ceced948d144/) in /var/www/html/sandbox/1c8606cc4b48bc4fc247f31cdd94ceced948d144/index.php on line 1
At that point, tamas_dxw had a clue.
as opposed to
easyphp
, we have a writable subdirectory and we can callini_set
/ini_alter
so we could probably break out of open_basedir jail withphuck3
(or something from here) - tamas_dxw [emwtf]
And finally, we were able to read the file function.php
!
The point of phuck3
is to trick open_basedir
using chdir, as advised by the docs:
When a script tries to access the filesystem, for example using include, or fopen(), the location of the file is checked. When the file is outside the specified directory-tree, PHP will refuse to access it. All symbolic links are resolved, so it's not possible to avoid this restriction with a symlink. If the file doesn't exist then the symlink couldn't be resolved and the filename is compared to (a resolved) open_basedir.
The special value
.
indicates that the working directory of the script will be used as the base-directory. This is, however, a little dangerous as the working directory of the script can easily be changed with chdir().Source: php.net
We are actually allowed to modify our open_basedir
settings - as long as we are setting them to a directory we are allowed to access.
So let us do exactly that, in a totally innocent way.
chdir
into that new directory.ini_set('open_basedir','..');
That is totally legal, because we are allowed to access the parent directory anyway.open_basedir
./
directory.ini_set('open_basedir','/')
;open_basedir
restriction.Of course we can now read function.php
:
xxxxxxxxxx
function waf($data='')
{
if (strlen($data) > 35) {
return true;
}
if (preg_match("/[!#%&'*+,-.: \t@^_`|A-Z]/m", $data, $match) >= 1) {
return true;
}
return false;
}
function initShellEnv($dir)
{
ini_set("open_basedir", "/var/www/html/$dir");
}
Now we can understand why that array bypass worked.
Firstly, strlen
is applied to the data. If the data is an array, it will be implicitly converted to string. And "Array"
is shorter than 35 characters.
Secondly, preg_match
checks for violating data with a regex on a target string.
preg_match()
returns1
if the pattern matches given subject,0
if it does not, orFALSE
if an error occurred.Warning This function may return Boolean
FALSE
, but may also return a non-Boolean value which evaluates toFALSE
. Source: php.net
So if an error happens within preg_match
, it will return something that can be interpreted as an integer of value 0
. Which is not >= 1
.
One file_get_contents('/flag')
later and we had... something.
wtf, there's a file at
/flag
but it's garbage - hyperreality [cr0wn]
send it anyway
- LucidBrot [flagbot]
Downloading it in a different way as an octet-stream gives us a gzip archive. And within that there's a flag.img
.
xxxxxxxxxx
$ file flag.img
flag.img: Linux rev 1.0 ext2 filesystem data (mounted or unclean), UUID=d4d08581-e309-4c51-990b-6472ba249420 (large files)
Obviously a mountable filesystem. But when we mounted it, all we got was an empty drive with only a lost+found
dir which was empty as well.
There must be something hidden.
xxxxxxxxxx
$ binwalk file
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Linux EXT filesystem, rev 1.0, ext2 filesystem data (mounted or unclean), UUID=d4d08581-e309-4c51-990b-6472ba24ba24
46080 0xB400 PNG image, 728 x 100, 8-bit/color RGB, non-interlaced
46121 0xB429 Zlib compressed data, default compression
Right! There's some additional PNG image as well as some compressed data. Those files can be extracted using binwalk --dd=".*" file
.
And in the PNG resides the flag!