
Подробный технический разбор PHP2Shell: объединение двух уязвимостей ядра PHP — краха из-за нехватки памяти и use-after-free — в цепочку с heap grooming, утечками ASLR и удалённым выполнением кода.

Я начал писать это исследование 26 октября 2026 года и пока не совсем уверен, когда его закончу.
На момент написания этой работы информации о PHP2Shell по-прежнему очень мало, особенно когда речь идёт о детальном техническом анализе уязвимостей и процесса эксплуатации.
Честно говоря, мне надоело находить объяснения, которые лишь скользят по поверхности и не показывают, что на самом деле происходит внутри. Я хотел сделать нечто иное: полноценный, глубокий технический разбор, который исследует весь процесс, рассматривая каждую важную деталь и объясняя не только ЧТО происходит, но и ПОЧЕМУ и КАК это происходит.
Это исследование основано на моём собственном понимании исходного кода PHP, моих экспериментах и расследованиях, которые я проводил, пытаясь разобраться в этих уязвимостях. Я хотел собрать все эти фрагменты воедино, поделиться своими находками и, надеюсь, сделать некоторые из лежащих в основе концепций более понятными для других.
Одной из моих главных целей было сделать материал доступным, не жертвуя технической глубиной. Я хочу, чтобы человек с базовыми знаниями C и общим опытом программирования мог следить за изложением, даже не имея предыдущего опыта в низкоуровневой эксплуатации памяти.
Я также надеюсь, что это исследование поможет прояснить некоторые вещи, которые сейчас объяснены плохо или поняты неверно. Однако хочу быть откровенным: это моё собственное техническое понимание уязвимостей, а не официальный анализ и не безошибочный справочник. Я всё ещё учусь, исследую и сам соединяю точки. По пути могут встречаться ошибки, неточности или вещи, которые я понял неправильно.
Если вы заметите что-то неверное, у вас есть другая интерпретация или вы считаете, что что-то заслуживает дальнейшего изучения, пожалуйста, не стесняйтесь связаться со мной. Я искренне буду признателен за исправления, обсуждения и вклад, которые помогут сделать это исследование более точным и полезным для всех.
В конце концов, весь смысл публикации этого материала — учиться вместе и помогать большему числу людей понять, что на самом деле происходит под капотом.
Уже знакомы с основами? Переходите сразу к интересующей вас части:
Если вы новичок в эксплуатации кучи или хотите получить полный опыт, просто продолжайте читать. Я писал это так, чтобы читать от начала до конца.
Есть несколько вещей, которые стоит держать в голове, прежде чем мы начнём. Они станут особенно полезны по мере продвижения в исследовании. Как я упоминал ранее, я хочу, чтобы это руководство было доступно каждому, поэтому я потрачу время на объяснение даже самых базовых концепций. Если вы уже знакомы с ними, смело пропускайте!
В типичной программе память организована в различные области, включая стек и кучу.
Например, в 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
}
Здесь a — локальная переменная, содержащая значение 30. Указатель b также является локальной переменной, но он содержит адрес целого числа, выделенного в куче.
PHP написан в основном на C, поэтому нам нужно понимать, как работает выделение памяти.
ОС предоставляет процессу собственное виртуальное адресное пространство. Затем распределитель памяти процесса управляет выделением памяти в пределах доступной ему памяти.
Например, в C++ у нас есть new, malloc() и другие способы выделения памяти.
Распределители часто пытаются повторно использовать освобождённую память вместо того, чтобы каждый раз запрашивать новую память у ОС.
Кроме того, когда время жизни объекта заканчивается, данные, хранившиеся в его памяти, не обязательно стираются немедленно. Эта память может быть просто помечена как доступная для повторного использования.
Чтобы лучше это понять, давайте кратко рассмотрим следующий код:
#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;
}
Здесь, когда мы выводим a и b, мы можем обнаружить, что они содержат один и тот же числовой адрес памяти.