
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<?phperror_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.
xxxxxxxxxxerror_reporting ([ int$level ] ) : intThe 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
levelis 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_alterso 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:
xxxxxxxxxxfunction 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()returns1if the pattern matches given subject,0if it does not, orFALSEif 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
/flagbut 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.imgflag.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-6472ba24ba2446080 0xB400 PNG image, 728 x 100, 8-bit/color RGB, non-interlaced46121 0xB429 Zlib compressed data, default compressionRight! 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!