
In-depth technical walkthrough of PHP2Shell: chaining two PHP core CVEs from an out-of-memory crash and use-after-free into heap grooming, ASLR leaks, and remote code execution.

I started writing this research on October 26, 2026, and I'm not entirely sure when I'll finish it.
At the time of writing, there is still very little information available about PHP2Shell, particularly when it comes to detailed technical analysis of the vulnerabilities and the exploitation process.
Honestly, I was getting tired of finding explanations that only scratched the surface without actually showing what was happening internally. I wanted to do something different: a proper, in depth technical walkthrough that explores the entire process, examining every important detail and explaining not just WHAT happens, but WHY and HOW it happens.
This research is based on my own understanding of the PHP source code, my experiments, and the investigations I've conducted while trying to understand these vulnerabilities. I wanted to put all these pieces together, share my findings, and hopefully make some of the underlying concepts easier to understand for others.
One of my main goals was to make this accessible without sacrificing technical depth. I want someone with basic C knowledge and general programming experience to be able to follow along, even without any previous experience in low level memory exploitation.
I also hope this research can help clarify some of the things that are currently poorly explained or misunderstood. However, I want to be transparent: this is my own technical understanding of the vulnerabilities, not an official analysis or an infallible reference. I'm still learning, investigating, and connecting the dots myself. There may be mistakes, inaccuracies, or things I've misunderstood along the way.
If you notice something incorrect, have a different interpretation, or think something deserves further investigation, please don't hesitate to contact me. I'd genuinely appreciate corrections, discussions, and contributions that could help make this research more accurate and useful to everyone.
After all, the whole point of sharing this is to learn together and help more people understand what's actually happening under the hood.
Already familiar with the basics?? Jump straight to the part you care about:
If you're new to heap exploitation or want the full experience, just keep scrolling. I wrote this to be read start to finish.
There are a few things everyone should keep in mind before we dive in. They'll become particularly useful as we progress through the research. As I mentioned earlier, I want this walkthrough to be accessible to everyone, so I'll take the time to explain even the more fundamental concepts. If you're already familiar with them, feel free to skip ahead!
In a typical program, memory is organized into different regions, including the stack and the heap.
For example, in C++:
int main() {
int a = 30; // Typically stored on the stack
int* b = new int(30); // b is typically on the stack,
// but the integer it points to is on the heap
}
Here, a is a local variable containing the value 30. The pointer b is also a local variable, but it contains the address of an integer allocated on the heap.
PHP is written mainly in C, so we have to understand how memory allocation works.
The OS provides a process with its own virtual address space. The process's memory allocator then manages allocations within the memory available to it.
For example, in C++, we have new, malloc(), and other ways to allocate memory.
Allocators often try to reuse freed memory instead of requesting new memory from the OS every time.
Also, when an object's lifetime ends, the data that was stored in its memory isn't necessarily erased immediately. That memory can simply be marked as available for reuse.
To better understand this, let's take a small look at the following code:
#include <cstdlib>
#include <iostream>
int main() {
void* a = malloc(64);
free(a);
void* b = malloc(64);
std::cout << a << "\n" << b << std::endl;
free(b);
return 0;
}
Here, when we print a and b, we may find that they contain the same numerical memory address.
Why? Because malloc() may reuse the memory previously allocated to a after we call free(a), avoiding the need to obtain another block of memory.
This isn't guaranteed to happen every time, but it's a common behavior.