0CTF/TCTF noeasyphp - Down the FFI Rabbit Hole (Part 1)

The post starts with my attempt at solving noeasyphp during 0CTF/TCTF 2020 and ends with my educational journey to a weaponized PHP FFI disable_functions bypass exploit.

The Challenge “noeasyphp”

This challenge was the sequel to “easyphp” where you are sent to a website and greeted with the dreaded PHP taunt code:

if (isset($_GET['rh'])) {
} else {

Without going too much into the previous challenge, the gist is that open_basedir was locked to /var/www/html which limited filesystem operations to that directory. In addition it appeared that the FFI::cdef() method was disabled which prevented us from calling into Libc. Finally, there were the following disabled built-in functions which severely limited any obvious open_basedir bypasses or easy system command access:


Fortunately - I had the general idea as to what needed to be done as my teammates had earlier solved ‘easyphp’ which ultimately relied on:

  • open_basedir bypass (or just wait for another team to bypass it xD)
  • read /flag.h to get / flag function name
  • $x = FFI::load(/
  • and finally $x->fLaG_FunCtion_NAmE() to read the flag

Going into ‘noeasyphp’ with this information I knew I had to call the flag function the same way - only this time I had no open_basedir bypass to read /flag.h. I didn’t know which function to call from /…. heh.

So, I decided that I needed a way to leak the function name and began investigating. Long story short I discovered that FFI::string([FFI\CData ptr], Size) had a memory leak that let you read an arbitrary number of bytes beyond the end of the CData object’s buffer. Interestingly, PHP does check the size… just not when the CData object is the type ZEND_FFI_TYPE_POINTER

// FFI::string() snippet from
	if (EX_NUM_ARGS() == 2) {
		if (type->kind == ZEND_FFI_TYPE_POINTER) { // No size check
			ptr = *(void**)cdata->ptr;
		} else {
			ptr = cdata->ptr;
			if (type->kind != ZEND_FFI_TYPE_POINTER && size > type->size) { // Size check
				zend_throw_error(zend_ffi_exception_ce, "attempt to read over data boundary");

Quickly using the method FFI:addr to make a pointer gives us our leak! (More info on FFI::addr later in part 2)

$x = FFI::new("char [4]");
$xPtr = FFI::addr($x);
echo FFI::String($xPtr, 200);

I spent some time leaking other teams’ requests and sorting through their payloads hoping to steal a flag or solution. I decided to share the leak in my team channel (it was quite late) and woke up to my fellow team member Ninja3047 having leaked the flag function name and getting the flag!

But I wasn’t satisfied - this leak peaked my interest and I decided to investigate further.

See part 2 on my personal blog @