PHP-FPM local root vulnerability

We discussed this vulnerability during Episode 96 on 02 November 2021

A privilege escalation to root in PHP FPM from a worker process where the attacker has arbitrary memory read/write and has escaped the PHP sandbox.

The core issue is in the parent (root) process keeping some of its data in a shared memory region that is shared with the less privileged user/worker processes. The shared memory itself makes sense, workers track their status and FPM can look in on it and aggregate it. Specifically, there is an array of pointers to the status structures that can be tampered with by the workers also.

Side-Effects of Killing a Worker

There are two important side effects that come in the process of killing a worker process. FPM will see the process died, free its structure, and then allocate a new structure and worker to replace the dead one. Remember that the attacker controls the pointer FPM uses to look up these structures.

  1. Clear 1168 bytes. This happens as a normal part of the freeing process. FPM checks the dead process structure, see it was in use, and will memset the structure resulting in a 1168 byte write of 0s. This happens if the ->used field is not zero.
  2. Set-0-to-1 after the free, FPM will try to alloc a new structure for the new proc. In this process is checks if the ->used field is 0, if it is it sets it to 1 and uses that structure.

Injecting data into the root process

Now this primtive isn’t terribly powerful but it is enough to start making some changes within the root process that can be useful. The author starts by turning on the catch_workers_output setting, which is off (0) by default. Creating a way to inject content into the root process heap (stdout/stderr from worker processes will get buffered by the root process before being written to a log file).

Heap Overflow

Being able to control data on the heap, along with these two primtives is enough to start considering a heap-exploit. (There is a section here about exploit stability and preventing any further workers from spawning during exploitation that I’m not summarizing)

Ultimately the author turns the 0 to 1 primtives into a heap overflow by targeting the zlog_stream structure which is created for every worker (though not initalized until data is written to stderr). By corrupting the buf.size and len fields which track the size of the backing buffer, and number of bytes already written respectively an overflow situation could be created by causing FPM to think it has more room to write.

** Arbitrary write**

Using the overflow they can target another worker process’s zlog_stream structure, this time replacing its buf.data (the backing buffer for stderr data) address with the desired memory address. Then writting on that processes stderr, will result in write relative to that address.

Code Execution

From this point, the write can target function pointers to get code execution. There is little concern regarding ASLR since worker processes are forked from the root process, so the children know the memory mapping of the parent.