Getting Arbitrary Read/WriteFollowing 0CTF...and some much needed sleep.. I went back to playing with FFI - since if it was that easy to leak memory then there had to be some other interesting things, right? To my surprise `FFI::memcpy()` was ALSO missing bounds checks on the destination when the `dst` was of type `ZEND_FFI_TYPE_POINTER` (https://github.com/php/php-src/blob/php-7.4.7RC1/ext/ffi/ffi.c#L4286):
This combined with our OOB read from earlier gave us OOB read AND write. Naturally the next step was to turn these into arbitrary read/write primitives. I also saw this as a good chance to learn some PHP internals and pwning. All we really had to do was find a way to control the pointer value that `FFI::String` and `FFI::memcpy` would read from and write to - it turns out that FFI makes this very easy for us!
Reintroducing `FFI::addr()`! According to the PHP docs:
Creates an unmanaged pointer to the C data represented by the given FFI\CData. The source ptr must survive the resulting pointer. This function is mainly useful to pass arguments to C functions by pointer.
Great - but would it really just give us a pointer to the CData object or would it do some internal trickery (no really -- I didn't know anything about PHP internals before this) ?
For this we had to dive into FFI internals a bit and see what the `FFI::addr` function was doing under the hood:
and for this to make sense you need to understand what the cdata structure looks like:
So what `FFI::addr` does is create a new CDATA object, place the cdata->ptr from your current object into the new object's ptr_holder, and finally take the reference to your original object's ptr_holder and place it in the new object's ptr. But what does this mean for us in terms of the exploit?
Well if we create a CDATA object and call `FFI::addr` on it we are given a new object where the value is a pointer! Now if we call `FFI::addr` again on the resulting pointer we are given a pointer reference to a CDATA object itself! Finally - we can directly overwrite the CDATA object pointer itself - which when combined with the OOB read and OOB Write gives us an effective arbitrary read/write.
Below is the example read/write along with some helper functions. I should also mention I use FFI::cast to easily convert the pointer to an integer (there's surely better ways to do everything on this blog post - let's get that out of the way now).
disable_functions Bypass and Calling System()With an arbitrary read/write it was time to actually pwn PHP and call system() with an arbitrary command. This was quite a learning process for me and led me down many hours of reading Zend source code, fighting with GCC security features, and staring at GDB.
I will try to keep this section relatively short. I started this journey looking at https://raw.githubusercontent.com/mm0r1/exploits/master/php7-backtrace-bypass/exploit.php with the intention of crawling up my CDATA object, getting a .text leak, walking back to the ELF header, parsing zif_system's address and executing it. This turned out to be the best way forward on the default PHP7.4 ubuntu installs - but of course I compiled the latest PHP myself from master and spent hours dealing with SCOP [https://bitlackeys.org/papers/secure_code_partitioning_2018.txt] (thanks dreamist!). Essentially this placed gaps between each PHP binary memory mapping making it impossible to walk back pages with a .text leak to parse the ELF header. I created a work around that worked for my local build --- you will see it in the final exploit. Will it work on other builds... probably not. For the rest of this I will focus on the exploit using the "parse elf header" technique.
zif_system() LeakIn PHP, many of the builtin functions are exposed from C to the PHP world using the C macro "PHP_FUNCTION" which prepends "zif_" to the internal name of the PHP function. So if our goal is to call system("cat /etc/passwd)" we really want to call zif_system() internally. This requires finding the address of zif_system.
Our first requirement was to get a .text section leak for the PHP binary. This would be our ticket to resolving the holy `zif_system` address. Fortunately for us our CDATA object has a pointer to a list of function handlers :). Our goal is to use our read to read a handler's address which would be in the php image. In the standard PHP build we had to read further into the handler list to get PHP .text pointer and not a ffi.so .text pointer.
With a valid .text leak (and a build of PHP not hardened via SCOP - we can rely on "mm0r1"'s method of parsing the elf header to leak zif_system. For the sake of brevity, you may reference my final exploit to see this process.
Controlling RIP - Calling zif_system with complete function controlWith a proper zif_system leak our next step was to call zif_system with our controlled args. Now I spent a lot of time here reading up on previous PHP exploits - but all I could find where Use-After-Frees that had an easy way to create a malicious closure object in memory which essentially allowed them to call a php function by address with the proper argument.
Despite my best effort I could not find a way to overwrite a closure. Naturally my first thought was to overwrite the handlers we saw earlier... unfortunately though the first argument was a bit of a pain to control as it was always a pointer to our own CDATA object. Great - we control that...although not so much. Before I will mention that zif_system is not simply expecting a C string of the system command to call. It accepts a pointer to a zend_execute_data object which we will focus on later.
Silly me thought I just had to pass "cat /etc/passwd" to zif_system.. but it makes much more sense that it would take an internal PHP structure as system() is called using a PHP string NOT a C string. This heavily complicated things.
In addition to the complex zif_system arg, the handlers were called in such a way that i could not corrupt the handler pointer being called (makes sense), but this means i lose some space in our controlled first argument. I needed complete control with out any FFI code paths messing up my call.
Admitably I spent many hours on this and in GDB trying to figure out my best course of action... also looking for ways to make PHP set up this argument object for me instead myself having to forge it in memory. Eventually I decided I had no choice but to fake the object -- but I still needed a 100% controlled call.
Taking a step back I remember that I'm exploiting FFI...which allows you to call functions... duh. Although I wanted this to be done WITHOUT use FFI::cdef() or FFI::load(). First I tried creating a function pointer with FFI, writing zif_system to its val, and calling it.
Example of my function pointer attempt:
As it turns out `zend_ffi_cdata_get_closure` actually checks if the CDATA type is `ZEND_FFI_TYPE_FUNC` which apparently ours was not. So using our write we fix that, write zif_system, then get 100% controlled arguments to any address. (We can actually do as many arguments as we want -- we can just add them to the function pointer declaration!).
Calling zif_systemFor the final step we need to create a proper zend_execute object to pass to `zif_system`. The `zif_system` call is pretty odd and uses ALOT of C macros which made reading it a PITA. What I ended up gathering is a pointer is passed to zend_execute object followed by the arguments itself. The arguments and zend_object go through a series of checks before the call is actually executed and we had to pass each check.
The argument in memory looks something like this:
So using how FFI tricks to allocate arbitrary memory we create the needed objects and eventually call zif_system!!!
Parting ThoughtsFirst and foremost - no this does not exit cleanly and no you do not get system output from the command. It's probably possible to do both.
As for the big question - does this use actual FFI bugs or am I just abusing FFI itself? PHP claims on their security bug report FAQ that nothing in FFI can be a security bug... additionally one might argue that these are not even bugs. It is just abuse FFI itself which already allows you to call into libc using FFI::cdef()... so. what's the point? Maybe there is none. Do I think these are bugs? Yes - there are many places where they do bounds checking in FFI.. so why bother with any if arb read/write is intended?
This is PHP's stance on this and I will respect their decision. Truth be told this journey was more about me learning how to pwn PHP and I was very happy in what I've learned. Particularly calling zif_system with a crafted argument - I could not find this technique used elsewhere.
For what it is worth I did report this to PHP prior to this post and they confirmed that this is not a security bug.
Final exploit... typos...notes..some struggles..and all. Note that in the original exploit I call each interesting finding that I abuse a "BUG" - looking back I'm not sure if calling these bugs is accurate. While I think the missing bound checks appear to be unintentional, perhaps FFI is designed this way on purpose. (MD5: 837034ab8198c13f97935215b65ad576)