organizers logo

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

Cloud Computing v1

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:

Considering those options one at a time:

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:

"$()/;<=>?[\]{}~ 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]

Digression

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]

Back On Track

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.

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.

Phuck3

as opposed to easyphp, we have a writable subdirectory and we can call ini_set / ini_alter so we could probably break out of open_basedir jail with phuck3 (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.

  1. Create a directory within our personal sandbox directory, because we have write access there.
  2. chdir into that new directory.
  3. ini_set('open_basedir','..'); That is totally legal, because we are allowed to access the parent directory anyway.
  4. Move into the parent directory. That's allowed by open_basedir.
  5. Do that again. And again. Until we're at the / directory.
  6. ini_set('open_basedir','/');
  7. We now can access any directory we want - as long as the permissions of the directory allow us, of course. We only circumvented the open_basedir restriction.

Waf Sucks

Of course we can now read function.php:

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() returns 1 if the pattern matches given subject, 0 if it does not, or FALSE if an error occurred.

Warning This function may return Boolean FALSE, but may also return a non-Boolean value which evaluates to FALSE. 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.

Get The Flag

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.

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.

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!