Thursday, May 28, 2015

Exploiting memory corruption bugs in PHP Part 3: Popping Remote Shells

This took longer than expected, but it's a journey worth taking! This is less descriptive than other blog posts, because I'd like to try the video format out once. AKA, I'm lazy :)

Disappointingly for some, this will be a guide to create a POC. See the video at the end for what a my automated & remote exploit looks like, as well as tips & tricks to get things working in a real environment.

I kept the app stupid to make life easy. It literally attempts to serialize whatever data is sent to the page, after base64_decoding it. More complicated exploits will require a little more fines :).

We need a way to execute arbitrary PHP code. Sure, we could try to inject shellcode, but that's not very creative, and much noisier than executing arbitrary PHP code (Also impossible in newer versions of PHP, unless it's very tiny). If you recall from Part 1, to execute code, we need to call php_execute_script AND zend_eval_string. However, since we're going to be attacking "remotely", we also need to find the executor_globals, and the JMP_BUF. More on those later.

In short, we need to find (In no particular order):
  • executor_globals
  • zend_eval_string
  • JMP_BUF
  • A way to write arbitrary data to the stack
Thankfully, some of these we can find relatively quickly, since they're in the binary. Let's go ahead and dump the String table of the PHP binary.



Great! Let's go ahead and start pulling addresses from it as well, and verifying that those addresses are correct in GDB.

Finding zend_eval_string's address
GDB showing that the address is legitimate
Finding executor_global's address
GDB showing that the address is legitimate
Awesome! Now, how do we find JMP_BUF? Well, while reading the source code for the _zend_executor_globals object, we find an interesting piece of information. A JMP_BUF pointer, called bailout. Let's look at this in GDB, and see if the address is useful.

Printing the _zend_executor_globals object
A better way to check the value of bailout
Well, we have an address, but what does this address point to? Is it even useful? Well, in PHP, JMP_BUF is used as a type of "try{} - catch{}" at the C level, but more on this later.

We're now only lacking one thing: A way to inject the malicious string onto the stack. Stefan's method in the 2010 Syscan talk will be discussed further in this blog post. Since we won't be freeing arbitrary memory addresses, what's the next best thing we could do? We could write to the stack, but how? And how do we guarantee that it won't be overwritten in the future? (Google is your friend here ;) ).

An RFC, specifically RFC1867.

This RFC allows POST requests with the multipart/form-data to set stack buffers, which aren't overwritten by PHP completely (due to a number of reasons). Let's try this out by posting "the usual" in a file. 


Awesome! We can write whatever we want to the stack now. But what do we want to write?
Hint: It's what we found earlier :)
Since we spent all this time figuring out what was where, we should probably use that! So, how do we lay it out? After some investigating, we need the stack to look like this:



Starting with the easy ones: We already have Zend_Eval String, since we found it earlier with readelf. Both the Ret Pointer and Zend_Bailout can be garbage, since we don't care about either of them (This will cause PHP to crash either way, without making the exploit more complicated). We can set the pointer_to_eval_string both times since we control the stack. So, here's what we have data on now:
  • POP; RET - ?????
  • XCHG EAX, ESP; RET - ????
  • Zend_Eval_String - 0x082da150
  • Zend_Bailout - 0x00000000
  • Pointer_To_Eval_String - 0xbfffda04
  • Ret Pointer - 0x00000000
  • Pointer_To_Eval_String - 0xbfffda04
Sweet! We have most of this already filled in, excellent! Unfortunately, it looks like we need some ROP gadgets. I prefer to use ROPGadget myself, but any gadget finding tool should suffice. We need to find XCHG EAX, ESP; RET (0x94 0xc3), and we also need to find POP EBP; RET (0x5d 0xc3). Once you have those gadgets, you're good to go! Let's go ahead and give it a shot!



Awesome! Now that we have these two locations (Why are the addresses so different? These are offsets!), we can complete the stack as follows:

  • POP; RET - 0x000e8e68
  • XCHG EAX, ESP; RET - 0x000057b7
  • Zend_Eval_String - 0x082da150
  • Zend_Bailout - 0x00000000
  • Pointer_To_Eval_String - 0xbfffda04
  • Ret Pointer - 0x00000000
  • Pointer_To_Eval_String - 0xbfffda04
Now that we have a complete view on how we want the stack to look, it's time to test this bad boy out. So, let's get to it! 


Hmmm, that didn't work out quite as expected, now did it? In fact, that looks like our code is trying to jump to our gadget (c394). Unfortunately, there's one more thing you have to know. There's a special object that's required for these gadgets to be useful without crashing PHP, otherwise the gadget's aren't useful at all. I'll save you the trouble of guessing/digging around in old code to figure it out. The object you need is SPLObjectStorage. Knowing this, we'll have to reformat all of our attacks from before. Once we have done that, then get the following after running through our exploit again, as shown below:

Screenshot taken from fully automated Python exploit. See video for details
This is my stopping point for this method, as it only affects older versions of php (With these same gadgets). For newer versions of PHP, keep reading :).

Fortunately not much changes. Recall that we looked up the address of php_execute_script AND the jmp_buf. We'll need both for this version of the exploit. 

jmp_buf is used by setjmp & longjmp, and saves the "environment" in case of an "unrecoverable" error. The jmpbuf is different depending on your architecture. For 32 bit, this is an unsigned array with 6 ints. If you're on 64 bit, there are 8 ints. Unfortunately, due to how this is implemented, there will be some digging in source code required for you to determine which position the registers are in within the jmp_buf. Here's an example of the jmp_buf layout. Of course, let's see how this looks in PHP...


Great! For my machine, the order of registers are: ebx, esp, ebp, esi, edi, eip. Since things worth doing in life aren't easy, this of course isn't a simple search! It looks like our edi & eip register are obfuscated. How're they obfuscated? By Glibc of course! Glibc has a macro called PTR_MANGLE. In the video we'll discuss how we crack the JMPBUF. 

Once we have it cracked, we need a way to overwrite and free memory. Thankfully, the same object (SPLObjectStorage) allows us to free memory remotely. All that's left is writing data to the stack. Just like in Part 2, we abuse the memory caching of PHP. We free some memory, write a small 7 byte string to fill it, and when php overwrites part of our data, we do it again. This 2nd overwriting allows us to write an arbitrarily amount of data on the stack (I did not test for values over 2048). This data that we want to write is very similar to what we used in the previous ROP example. We of course need to "encrypt" our values for PTR_MANGLE. Here's some example output:


With that said, here's the video!


NOTES FOR THE VIDEO: 
x86 Instruction Chart - http://sparksandflames.com/files/x86InstructionChart.html
Elf Header lowest 3 bits are 000
Elf layout - http://geezer.osdevbrasil.net/osd/exec/elf.txt
PMAP is your friend when trying to find the "Magic"
A look at PTR_MANGLE http://hmarco.org/bugs/CVE-2013-4788.html

Stay tuned for some iOS fun ;)

6 comments:

  1. if this is remotely done, shouldn't you be leaking some address from the remote server to get the addresses of your ROP gadgets? you get that locally.....
    what's the role of SPLObjectStorage?

    ReplyDelete
    Replies
    1. Yes, you would have to leak those remotely, but since we download the binary (in the video), you can run RopGadget locally :).

      Delete
    2. you've downloaded the whole binary? by leaking?
      the sound qualite is quite bad in the video:(

      Delete
    3. Yes, that's correct. And yeah, my headset was already packed, had to use the one on my laptop. Should be fixed in the next one.

      Delete
  2. ps: I just noticed the video, I'll check that too, maybe it gives me some answers.

    ReplyDelete
  3. This comment has been removed by a blog administrator.

    ReplyDelete