In the land of PHP you will always be (use-after-)free

We discussed this vulnerability during Episode 136 on 12 April 2022

A bug and exploit that hearkens back to old-school browser exploitation. The bug is a use-after-free in concat_function() for variable concatenation, which is abused in the PHP engine to escape disable_functions and open_basedir sandboxing.

The bug In PHP it’s possible to concatenate variables like strings using the .= operator. Internally this invokes concat_function(), which will try to process the operands and convert things like integers to strings if necessary. Objects however (such as arrays) are not supported for concatenation and trigger an error. In PHP it’s possible to define your own error handlers, which can get invoked in scenarios like this. In an error case, it’ll free and cleanup the first operand (the variable being concatenated to), but it won’t consider the fact that the user-specified error handler can swap out that variable for something else which will implicitly free the variable’s backing zval, which holds refcounting information and pointers to backing data.

Exploitation Abusing the bug gets you in a scenario where you can effectively overlap some arbitrary object with the UAF’d zval that’s getting destroyed. This is used to build two primary primitives, an arbitrary free and an infoleak. The arbitrary free simply involves overlapping a string which will get used as a zval in the cleanup code. The infoleak seems to be later in the cleanup code where a pointer gets written to the overlapped string, which is a string of null bytes.

By using some heap feng shui, objects can be allocated contiguously, and by leaking the pointer of a string, an adjacent object (such as a helper object with some inline properties) pointer can be leaked and free()’d. From here, they use the object properties and the ability to corrupt the backing pointers to derive arbitrary read/write primitives (also similar to browser exploitation).

With arbitrary read, they scan the heap for the basic_functions table that contains a mapping of functions to handlers, and search for the system() pointer. Then with arbitrary write, they smash a setup anonymous function handler to set it to the leaked system() pointer, bypassing the disable_functions sandboxing.