Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
Herramientas/GitHubGitHub/supersonic/tlpe
Seguridad AndroidEscalada de PrivilegiosMecanismos de PersistenciaAnálisis de VulnerabilidadesExplotaciónSeguridad MóvilExplotación de Binarios
GitHubsupersonic/tlpe

TLPE

CVE-2026-49881, un problema de lógica en la clase InCallController del servicio Telecom de Android 17 que permite que una aplicación sin privilegios obtenga ejecución arbitraria de código como UID 1000 system_server

Ver Repositorio
111hace 11 horasAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

PoC y análisis de CVE-2026-49881

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)

TLPE

Notas sobre el PoC

  • El PoC demuestra la obtención de ejecución de código en 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.
  • Una vez instalado el PoC, tocar el botón Start Exploit o realizar una llamada a través de la pila de telecom lo activará.
  • Compile el PoC ejecutando el 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.
  • El PoC registrará su propio certificado como certificado ancestro para el UID 1000 después de una explotación exitosa. Este estado persiste a través de las OTA, incluido el parche de la propia vulnerabilidad. Para limpiar su dispositivo después de una ejecución del PoC, debe presionar el botón "Uninstall" en el PoC (que limpia el certificado inyectado); no obstante, recomiendo encarecidamente usar su propio almacén de claves de lanzamiento para firmar un APK del PoC compilado durante las pruebas. (en lugar del que el PoC usa por defecto en TLPE/app/teststore.jks)
  • Tenga en cuenta que el PoC, después de entrar en system_server, estableciendo en en 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.

La salida esperada del logcat del PoC es:

root@kitploit:~
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...

Análisis

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í:

root@kitploit:~
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;
                }
                ...
            }
        }
}

Observe cómo serviceClassExists se ejecuta contra cualquier componente que declare un intent InCallService junto con el valor de metadatos android.telecom.CLASS_EXISTENCE_CHECK, no solo contra InCallServices válidos/habilitados. (esa verificación se implementa más adelante en la rama con getInCallServiceType e isServiceEnabled)

serviceClassExists se implementa como:

root@kitploit:~
/**
 * Verifies that the class for a given ServiceInfo exists within its package.
 * This prevents a system crash if a service is declared in the manifest but its
 * class was not included in the compiled code.
 * @param serviceInfo The ServiceInfo of the service to check.
 * @param userHandle The user under which to check for the service.
 * @return {@code true} if the class exists, {@code false} otherwise.
*/
private boolean serviceClassExists(ServiceInfo serviceInfo, UserHandle userHandle) {
    Log.i(this, "serviceClassExists check");
    try {
        Context packageContext = mContext.createPackageContextAsUser(
                serviceInfo.packageName,
                Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY, userHandle);
        ClassLoader classLoader = packageContext.getClassLoader();
        Class.forName(serviceInfo.name, false, classLoader);
        return true;
    } catch (NameNotFoundException | ClassNotFoundException e) {
        Log.w(this, "Skipping InCallService: class not found for " + serviceInfo.name);
        return false;
    } catch (Exception e) {
        Log.e(this, e, "Error checking for existence of " + serviceInfo.name);
        return false;
    }
}

Los peligros de createPackageContext con CONTEXT_IGNORE_SECURITY están bien documentados, y en este caso es el propio system_server el que lo ejecuta contra un componente no confiable solo para verificar si una clase existe en el DEX de esa aplicación. A primera vista, sin embargo, esto puede parecer seguro porque el contexto obtenido se usa en Class.forName con initialize=false; el desarrollador muy probablemente era consciente de este riesgo y especificó ese argumento para asegurar que la clase externa no se inicialice en system_server.

Desafortunadamente, esta precaución llega demasiado tarde: el daño ya está hecho por getClassLoader en el contexto externo. Si la aplicación atacante define un AppComponentFactory en su manifiesto como android:appComponentFactory, el método getClassLoader en un LoadedApk correspondiente a un contexto creado con CONTEXT_INCLUDE_CODE y CONTEXT_IGNORE_SECURITY primero recupera el cargador de clases predeterminado de la aplicación externa desde el disco y luego ejecuta tanto el constructor de la fábrica como su método instantiateClassLoader antes de devolver el cargador de clases.

Como esta clase es controlable por la aplicación atacante, esto conduce inmediatamente a la ejecución arbitraria de código en el contexto de Telecom.

Post-explotación

Para mostrar las consecuencias de la ejecución exitosa de código en system_server, el PoC demuestra reinstalarse a sí mismo como un componente persistente de system_server. (la técnica está adaptada de la técnica publicada en el PoC AbxOverflow / CVE-2024-34740 de Michał Bednarski)

Hacemos esto mediante:

  • Recuperar el objeto Signature de nuestra aplicación PoC reflejando dinámicamente en PackageManagerService.mSettings directamente una vez que estamos en system_server.
  • Recuperar el SharedUserSetting correspondiente a "android.uid.system" e inyectar el Signature del PoC dos veces en su getSigningDetails().mPastSigningCertificates con CertCapabilities.SHARED_USER_ID.
  • Forzar la desinstalación de la aplicación PoC y reinstalar una variante de la misma con android:sharedUserId="android.uid.system" y android:process="system" añadidos al manifiesto. (lo que también vacía la modificación volátil del paso anterior a packages.xml)

Esto hace que canJoinSharedUserId() en PackageSignatures pase debido a la coincidencia del historial de rotación y produce privilegios persistentes de system_server.

Nota final

Una pregunta que podría tener es por qué existe esta vulnerabilidad en absoluto; en particular, ¿por qué hay una verificación de existencia de clase optativa protegida detrás de un indicador de metadatos no documentado?

Es imposible decirlo con certeza, pero la respuesta probable a este misterio se puede encontrar profundizando un poco más en AOSP: tanto la verificación de clase como la comparación de metadatos probablemente se agregaron para tener en cuenta android.net.ConnectivityCallListenerService, que se estaba implementando aproximadamente al mismo tiempo.

Este servicio se definió inicialmente en el manifiesto del framework pero no se implementó en ningún lugar. (y cuando sí se agregó, estaba detrás del indicador de características Flags.FLAG_ENABLE_INCALL_SERVICE_API) Una vez que se observaron los bloqueos en las pruebas, alguien probablemente decidió implementar la corrección como una verificación reutilizable de "defensa en profundidad" en lugar de una excepción codificada; la definición del manifiesto de este servicio dice que define "android.telecom.CLASS_EXISTENCE_CHECK" para "indicar que la clase de este servicio puede no estar presente en todas las compilaciones" y "dirigir a Telecom para verificar la existencia de la clase antes de intentar vincularse". (y es por eso que estamos aquí ahora)

Descargar herramienta
también desactiva Play Protect
package_verifier_user_consent
-1
Settings.Global
  • Se ha validado en Pixel Android y AOSP, pero la etapa de ejecución de código debería funcionar en versiones de Android 17 personalizadas por OEM. La etapa de reinstalación como 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.