Blog
Hunting for hidden parameters within PHP built-in functions (using frida)
When I was working on PHP, I had found some "hidden" parameters to PHP built-in functions but had been too P/S: If you are looking for vulnerabilities, you can stop reading this post now. There is none. Let's just dive straight into what I mean by "Hidden parameters". The image below shows the API for the built-in function intlcal_from_date_time : One can typically call it as follows: intlcal_from_date_time("1pm") Notice that the documented API only allows for 1 input. Or does it? There's actually a 2nd undocumented parameter, that you can pass in to set it's locale. For example you can call it via: intlcal_from_date_time("1pm", "zh-Hant-TW") This returns an intlcalender object with its locale set to Chinese, Taiwan. The question one might ask is, why is there an undocumented parameter? I've no idea. Could it just have been the lack of documentation updates? Maybe. Does it do anything really fanciful ? Not in this case. It just sets the locale. However these hidden parameters or discrepancy in the documentation do present an additional attack surface area which is of interest to me for fuzzing. So the question now is: How does one go about discovering these hidden parameters systematically. To do that, one must understand how PHP API parses parameters internally within the zend engine. When a call is made to a PHP function, the parameters are being parsed and validated internally by Zend engine's zend_parse_parameters. In the case of intlcal_from_date_time(), lets' look at the source code within intl/calendar/calendar_methods.cpp:
On line 7, You will see that zend_parse_parameters is being called to parse and validate the inputs to intlcal_from_date_time(). It is being validated against the pattern "Z|s!". Here's how to interpreter the pattern: Z: it is expecting a required zend object ($dateTime in this case) | : anything after this is optional s: It takes in an optional string (Which is our hidden parameter Locale in this case) More information about the patterns can be found here So my methodology to discovering these hidden parameters systematically is as follows:
Before anyone gets too excited, there are only a handful of functions with hidden parameters, not all of them do anything interesting. I'll leave that as an exercise for the reader. However, this methodology of hooking into zend_parse_parameters does allow me to identify and build a database of exactly what Zend-engine is looking for when calling a PHP built in function. I've found this to be especially useful when I was fuzzing PHP, as the documentation often label a perimeter as "mixed" which can be quite ambiguous. Let me end off with a simple snipplet of my python + frida code to hook into zend_parse_parameters.
|
Double Free in Standard PHP Library Double Link List [CVE-2016-3132]
I found a double free vulnerability in the Standard PHP Library (SPL). While writing the exploit, I can't seem to find much write up on how PHP manages their heap internally. Through this blogpost I'll shed some light on this topic as well as my approach to exploit a double-free vulnerability like this within PHP.Root Cause Analysis The vulnerability is in SplDoublyLinkedList::offsetSet ( mixed $index , mixed $newval ) when passing in an invalid index. For example: <?php$var_1=new SplStack();$var_1->offsetSet(100,new DateTime('2000-01-01')); //DateTime will be double-freed When an invalid index (100 in this case) is passed into the function, the DateTime object is being freed for the first time in line 833: 832 if (index < 0 || index >= intern->llist->count) { 833 zval_ptr_dtor(value); 834 zend_throw_exception(spl_ce_OutOfRangeException, "Offset invalid or out of range", 0); 835 return; 836 } The second free occurs in Zend/zend_vm_execute.h:855 when it cleans up the call stack: 854 EG(current_execute_data) = call->prev_execute_data; 855 zend_vm_stack_free_args(call); PHP Internal Heap Management Internally within PHP, when doing heap allocations via calls such as ealloc(), it falls into 3 categories depending of the size: Small heap allocation (< 3072 bytes) Large heap adllocation ( < 2 megabytes) Huge heap allocation ( > 2 megabytes) Lets explore the small heap allocation/deallocation since that's what we are going to be using in this exploit. When dealing with memory handled by the small heap allocator, each chunk of memory is categorised into "bins" based of their size. For example:
When a memory chunk is freed internally within PHP via calls such as efree(), if the chunk in question is handled by the small heap deallocator, instead of releasing it back to the OS, it caches it and place it into an appropriate "Bin". This Bin is implemented as a single link list with freed chunks of memory being chained together. The first couple bytes of each freed chunk can be considered its header and it contains a pointer that points to the next freed chunk. Visually this is what it looks like in the memory: Step 0: Evaluate if this is feasibly exploitable. This vulnerability is triggered by trying to insert an object into an invalid SplDoublyLinkedList index. This triggers a fatal error as such: Fatal error: Uncaught OutOfRangeException: Offset invalid or out of range Upon the fatal error, PHP exits immediately. This prevents us from running any user code in "userland" after the double free. Thus in order for us to exploit this vulnerability, we need to trap the error and make PHP not exit immediately after the double free. This can be done via the set_exception_handler(); Step 1: We need to decide on what object we want to trigger the double free on. I have chosen to use SplFixedArray for this due to the following reasons:
Step 2: We need to do some heap massaging. First let's do some cleaning up and clear any free chunks of memory in the Bin associated with SplFixedArray's size. We can do that by allocating many instances of the object. This also ensures that the SplFixedArrays allocated are in contiguous chunk of memory
On line 5 above, we free the 50th SplFixedArray. This creates a hole in the contiguous chunk of memory allocated and can be visualized as: We immediately allocate a new SplFixedArray (which causes it to occupy the 50th slot) and trigger the double free vulnerability on it:
The reasons for all these heap manipulation is to ensure that the double free vulnerability is triggered on a location that is part of my controlled contiguous chunk of memory. This gives me relative good control of the memory layout as well as references to its neighboring chunk of memory (49th and 51st SplFixedArray). After the first free, this is what the memory looks like: After the 2nd free, this is what the memory looks like: Step 3: Let's now exploit this abnormally in the heap. Notice that there's only 1 free chunk of memory? However the link list now has two arrows which points to the same chunk of memory. At this point, the heap is corrupted and PHP think there's 2 free chunk of memory when there's only 1. This means that we can allocate 2 free chunk ( from Bin #12) and they will occupy the same memory space:
In the code above we allocate a string (line 2) and an object mySpecialSplFixedArray (line 3) which will both occupy and the same space between the 49th SplFixedArray and 51st SplFixedArray. Notice that in line 3 I allocated mySpecialSplFixedArray which is just an extended class of SplFixedArray but with the offsetUnset method being overwritten. To understand why, lets take a look at the PHP internal structure of a String vs SplFixedArray:
Step 4: Gaining Code execution. At this point:
Assuming I want to execute code at 0xdeadbeef, here's the approach I'll take:
Achieving (1) is easy because we can write into the 51st SplFixArray and sore our fake Handler structure there.Achieving (2) is also easy because $s and mySpecialSplFixedArray shares the same memory space. The tricky part here is that when we create the fake Handler structure, we do not know the address of where is the exact address fake structure. Without knowing the address, we can't point mySpecialSplFixedArray to our fake handler structure. The solution to that would be to free the 51st and 52nd SplFixArray: Due to this free, the first 8 bytes of the 51st SPLFixedArray chunk now points to the 52nd SPLFixedArray memory chunk. By using $s to read into the first 8 bytes of the 51st SplFixedArray, one can figure out the exact memory address that one is in. Here's the exploit code in its full glory:
Original bug report can be found here |
Exploiting CVE-2016-1903: Memory Read via gdImageRotateInterpolated
Vulnerability Background This isn’t a vulnerability with a large impact, but it’s one that I thoroughly enjoyed exploiting. Hence this blogpost. The issue is in the PHP function imagerotate() : The function takes in an $image, rotates it by $angle degrees and fills any empty space left over as a side-effect of the rotation with $bgd_color.
Here’s an example of how the function is used in practice:
The code creates an image as follows: The crux of the vulnerability is in the function’s handling of the $bgd_color parameter to imagerotate(). That is an index to the Color Pallette. Looking at the PHP code, we can see that the palette is stored within the image’s gdImageStruct structure as red, green, blue and alpha arrays:typedef struct gdImageStruct { /* Palette-based image pixels */ unsigned char **pixels; int sx; int sy; /* These are valid in palette images only. See also 'alpha', which appears later in the structure to preserve binary backwards compatibility */ int colorsTotal; int red[gdMaxColors]; int green[gdMaxColors]; int blue[gdMaxColors]; int open[gdMaxColors];...................... int alpha[gdMaxColors]; The size of each array is gdMaxColors, defined elsewhere as 255. The function does not check that the passed in $bgd_color falls between these arrays’ valid ranges of 0 - 255, potentially resulting in an out of bound lookup to the arrays. Thus, if we run the following code, where $bgd_color is a large number (0x6667), we get the following image: $mypic2=imagerotate($mypic,45,0x6667); Notice the maroon color in the background? The color came from the undefined memory located at red[0x6667], blue[0x6667], green[0x6667] and alpha[0x6667]. By "deciphering" the background color, we can attempt to determine the bytes that were at that memory location, allowing us to perform an arbitrary memory read! Exploiting the Vulnerability to Read MemoryHere's a high level visualization of how the image's color palette looks like in memory: gdTrueColorAlpha(r, g, b, a) (((a) << 24) + \ ((r) << 16) + \ ((g) << 8) + \ (b)) Thus given a background color, we can obtain the underlying bytes of memory that were at that color’s memory location by:
Step 1 should be easy right? Looking at the PHP code above, breaking a color back into its RBG Alpha compoments should be as easy as a couple of bit shifting here and a couple of bit shifting there. Apparently Not! This is where it gets tricky (and fun)! There are a couple of issues: Issue 1: PHP's RGBA internal representation By convention, each RGBA component are typically represented by 1 single byte. For example, this color with 50% opacity is represented as 0x80FF9000 (ARGB convention) where:
If PHP had used this typical convention of 1 byte per component, reversing a background color back into the underlying memory would be extremely trivial. Instead, PHP stores each RGBA component as 32-bit Integers (4 bytes), despite only requiring 1 byte! Thus, this is a more accurate high level visualization of a PHP image's color palette, shown when computing the background color at index x: Since PHP uses 4 bytes per color component, the formula to calculate the background color can be better visualized as: The trick is to correlate a whole bunch of background colors. For example, given a background color computed from palette[x], we will know the LSB of Red[X]. This is denoted by ! in cell AX[1st Byte]: Now what if we increase the palette index by 256 and compute the background from index x+256? This will give us BX[1st Byte] (This is the LSB of RED[X]). Since we now know BX[1st Byte], we can infer AX[2nd Byte] because background color Palette[X][2ndByte]= AX[2nd Byte] + BX[1st Byte] : If we continue on another 256 bytes and compute the background from the x+512 index, we can infer even more bytes: Thus this is how we can solve the issue of PHP using 32-bit integers for RGBA component representation and infer bytes in the original bytes in out-of-range memory. The only other tricky thing to take note of when inferring the bytes is to consider potential carry bits.Issue 2: PHP converts the palette-based image to a true color image after rotation The vulnerability is triggered by imagerotate() and the vulnerable path is only taken when the image is a palette-based image. This is how the code is implemented in PHP:
Notice the following:
The Implication of line 9 is that for each image, you can only trigger the vulnerable code once before it is being coverted to a true color. You might ask, why can't we just generate a bunch of different images and trigger the vulnerable code once on each image? This is because, each image gets allocated in different parts of PHP heap, meaning that it would virtually be impossible to read from the same contiguous out-of-range memory using the above technique. To solve this issue, here is my pseudo code:
Issue 3:Alpha array is at a weird offset Notice that the arrays for Red, Blue, Green are all contiguous ? Not so for the Alpha array. It is at an offset which makes things a pain. This is something that has to be considered when writing the exploit. Original bug report can be found here. |
CVE-2015-3329: POC for buffer overflow in PHP phar_set_inode
Background PHP has the built-in Phar & PharData functionality since 5.3.0. These are used to manipulate the following archive types: tar,zip & phar. I found this vulnerability through fuzzing. Technical Detail This is a standard BOF in phar_set_inode() @ phar_internal.h: static inline void phar_set_inode(phar_entry_info *entry TSRMLS_DC) /* {{{ */{ char tmp[MAXPATHLEN]; int tmp_len; tmp_len = entry->filename_len + entry->phar->fname_len; memcpy(tmp, entry->phar->fname, entry->phar->fname_len); memcpy(tmp + entry->phar->fname_len, entry->filename, entry->filename_len); entry->inode = (unsigned short)zend_get_hash_value(tmp, tmp_len);} The vulnerability occurs because it didn't check that tmp_len is < MAXPATHLEN. On my x64bits ubuntu MAXPATHLEN=0x1000. Exploiting the vulnerability is trivial since attacker controls entry->filename and entry->filename_len. The only thing to note is that since tmp_len is below tmp[] on the stack, when overwriting tmp_len, it needs to ensure that the value being overwritten with is within reasonable limits, otherwise it would crash when zend_get_hash_value() is called. There are multiple pathways to trigger this:
I've found that the easiest (read: boring) way to trigger it was via ZIP. The more interesting path would be via Tar for the following reasons:
Even though it validates the checksum (line 116), as long as the file ends with .tar (line 118), it would always pass the validation. Cheers PHP for being so lenient ;)
POC for exploits can be downloaded here. Written for:
Bug report can be found here |
CVE-2015-2783: Exploiting Buffer Over-read in Php's Phar
Background I found this vulnerability while I was assessing the security of the phar extension. When parsing a phar file, it is possible to trigger a buffer over-read condition and leak memory information. This vulnerability is interesting as it's not a typical exploitation just by controlling the read size. Affected version are PHP < 5.6.8RC1 Technical Details Phar files metadata are stored in php serialized format. When processing a phar file, php attempts to unserialize the medatadata in phar.c:
php_var_unserialize() is the same function that is called when unserialize() is invoked in PHP "user-land". Within Php_var_unserialize () there is a sanity check to ensure that p does not go beyond p + buf_len:
When we start unserializing a string in the format: s:<len>:"<Data>", lines 890-900 is essentially a loop to extract out <len>. As long as <len> is a digit, it would keep looping even if YYCURSOR goes beyond p+buf_len. When YYCURSOR goes beyond max (aka p+ buf_len or YYLIMIT), it results in an integer underflow on line 907. Thus the sanity check on line 908 will always pass. There's also some format checks:
Lines 900, 915 to 920 checks the data format for the tokens as highlighted : s:<len>:"<Data>" In essence, what this means is that once YYCURSOR goes beyond max when extracting <len>, <len> can be as large a number as you want and it will unserialize to a string successfully as long as it's in the format s:<len>:"<Data>" This is the state you want it look like during exploitation: At this point one might ask, how do we ensure that the ending byte is " since it's in a memory region beyond our control? Well you have 1 in 255 chance of strike gold. But one possible way to exploit it is just to keep "hammering" the various values of xxxxxxxxxxxx . Sooner or later you will encounter a ". One might also ask, why can't I trigger this buffer over read through a typical unserialize call? For example why can't we trigger it via unserialize("s:0010") + some heap massaging? Why is it only vulnerable when we unserialize through phar?
Here's a screenshot of successful exploitation to leak memory a la Heartbleed style: After Thoughts I exploited this by using a serialized string type. It would be interesting to see if we can get code execution using other types. The full bug report could be found here. |
CVE-2015-2331: ZIP Integer Overflow Root Cause Analysis
I was fuzzing PHP zip extension and found the first crash on the 28th day. Sure took me a while. The vulnerability is an integer overflow when parsing Zip64 on zip_dirent.c:113: else if ((cd->entry=(struct zip_entry *)malloc(sizeof(*(cd->entry))*(size_t)nentry)) == NULL) { nentry is obtained from the zip file. On my x64, sizeof(*(cd->entry)) = 0x20 bytes long. Thus when nentry > 0x7FFFFFFFFFFFFFF, this will results in an integer overflow and malloc will allocate less memory then required. Further down in zip_dirent.c:119 for (i=0; i<nentry; i++) _zip_entry_init(cd->entry+i); The most likely reason it took 28 days of fuzzing to find this is because of zip_open.c:113 /* number of cdir-entries on this disk */ i = _zip_read2(&cdp); /* number of cdir-entries */ nentry = _zip_read2(&cdp); if (nentry != i) { _zip_error_set(error, ZIP_ER_NOZIP, 0); i = total number of entries in the central directory on this disk nentry = total number of entries in the central directory Random bit flipping will need to flip the both the i and nentry field to the same overflow values before going down the crash path. After reporting this bug, I realized that it has a wider implication as this is a bug in libzip. PHP is naturally affected as it has a embedded (and modified) version of libzip in it's code. Full bug report can be found here. |