Deserialization: POP-цепочка → RCE через file write
unserialize() без allowed_classes + классы с magic-методами (__destruct, __toString) позволяют собрать POP-цепочку, которая пишет PHP webshell в /static и даёт RCE.
hardphpPro
Задача
# Lab: POP-цепочка через unserialize()
## Сценарий
SecureShop — небольшой интернет-магазин на нативном PHP. Чтобы помнить выбранную пользователем тему оформления и язык интерфейса между запросами, приложение сохраняет настройки в cookie `user_prefs` как `base64(serialize($object))`. При заходе на `/profile` приложение декодирует cookie и вызывает `unserialize()` без `allowed_classes`, чтобы восстановить объект `UserPreferences`.
Это классический антипаттерн: `unserialize()` поверх данных, которые контролирует клиент (cookie). PHP по дороге восстановления может инстанциировать **любой** класс из autoload — не только `UserPreferences`. Если у такого класса есть magic-методы (`__destruct`, `__wakeup`, `__toString`, `__call`), они автоматически отработают по ходу жизненного цикла объекта.
## Что такое POP-цепочка
**Property-Oriented Programming (POP)** — техника эксплуатации, которая использует уже существующие в кодовой базе классы как «гаджеты». Атакующий не пишет свой PHP-код на сервере — он лишь собирает «нужный набор объектов с нужными полями», и при их восстановлении PHP сам по цепочке вызывает методы:
1. Создаётся объект `A` — срабатывает `A::__destruct()`.
2. `__destruct()` вызывает метод другого объекта `B`, ссылка на который лежит в поле `A->next`.
3. Метод `B->doSomething()` использует поле `B->target` как имя файла / shell-команду / SQL-запрос.
Итог: ни одной собственной строки PHP-кода атакующий на сервер не отправлял — он лишь подобрал значения полей. Это и есть POP — программирование «через свойства объектов», не через инструкции.
## Magic-методы PHP, которые автоматически запускаются
- `__wakeup()` — вызывается сразу после восстановления объекта `unserialize()`.
- `__destruct()` — вызывается при завершении скрипта или сборке мусора.
- `__toString()` — вызывается при приведении объекта к строке (`(string)$o`, конкатенация, `echo`).
- `__call($name, $args)` — вызывается при обращении к несуществующему методу.
Чем больше магии у класса, тем выше шанс, что он годится как gadget. Готовые цепочки для популярных фреймворков (Laravel, Symfony, Monolog, Guzzle) собраны в проекте **PHPGGC** — атакующий может выбрать нужную одной командой.
## Гаджеты в SecureShop
В исходном коде магазина остался старый cache-слой с двумя классами:
- `CacheEntry` — элемент файлового кэша, в `__destruct()` сбрасывает буфер на диск через помощник: вызывает `$this->writer->persist($this->key, (string)$this->data)`.
- `FileWriter` — helper, метод `persist($key, $data)` вызывает `file_put_contents($this->dir . '/' . $key, $data)`.
Эти классы больше нигде не инстанцируются, но остались в autoload (зарегистрированы в `composer.json` → `autoload.files`). PHP-сериализация позволяет собрать вложенный объект `CacheEntry`, внутри которого `writer = FileWriter(dir="/lab/static")`, `key = "shell.php"`, `data = "<?php system($_GET['c']); ?>"`. Когда скрипт завершает работу, PHP вызывает `__destruct()` у всех объектов — и `CacheEntry::__destruct()` через `FileWriter::persist()` запишет произвольный файл от имени `www-data`.
Директория `/lab/static/` раздаётся Apache напрямую (без роутера), а `.htaccess` разрешает исполнение PHP в этой папке — значит, записанный `.php` файл будет исполнен при первом же HTTP-запросе. Дальше — обычный webshell с RCE.
## Цели
1. Получить RCE на сервере через подделанное cookie `user_prefs`.
2. Прочитать CTF-флаг из переменной окружения `LAB_FLAG` (команда `env` или `printenv` покажет его).
## Данные
| Параметр | Значение |
|----------|----------|
| Обычный пользователь | `demo` / `demo` |
| Директория static | `/lab/static/` (раздаётся Apache как `/static/...`) |
| Endpoint логина | `POST /login` |
| Endpoint, читающий cookie | `GET /profile` |
| Cookie с payload | `user_prefs` (значение — `base64(serialize(...))`) |