我们的 AI 引擎 Neutron 在加州大学伯克利分校的 CyberGym 基准测试中取得了 96.75% 的成绩。 了解更多

安全

安全

Android Intent 重定向:攻击与修复

Intent 重定向如何让攻击者触及未导出的 Android 组件、通过 setResult() 泄露数据并滥用 PendingIntent,以及防范它的六种方法。

Intent 重定向是 Android 应用中的一类漏洞,恶意行为者借此诱使受害应用代其转发或分派一个 Intent。由于被转发的 intent 以受害应用的身份和权限执行,攻击者无需持有任何特殊权限,就能触及未导出的组件、窃取敏感数据或提升权限。

这类漏洞在 Android 漏洞赏金项目中一直被列为影响最大的发现之一,并曾波及包括 TikTok 和多款 Google 第一方应用在内的知名应用。


Intent 重定向的工作原理

攻击的核心在于利用一种代理模式(proxy pattern):受害应用从外部(受攻击者控制的)来源接收一个 intent,并在未经充分校验的情况下,使用该 intent 的一部分去启动另一个 activity、发送广播或绑定 service。

攻击流程

Android 中的 Intent 重定向让攻击者滥用已导出的代理组件来转发不受信任的 Intent,触及未导出的目标、通过 setResult() 泄露数据、借助 URI 授权窃取内容并劫持认证流程——并附带实用的纵深防御缓解措施。

核心误用模式

最简单的脆弱代码如下所示:

// VULNERABLE — Unvalidated intent forwarding
Intent forward = getIntent().getParcelableExtra("next_intent");
startActivity(forward);

受害方盲目信任 next_intent 这个 extra,并启动攻击者指定的任意组件——包括受害方自己的未导出 activity。


脆弱的代码模式

1. 未经校验的 Intent 转发

// VULNERABLE
class RouterActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val target = intent.getParcelableExtra<Intent>("target")
        target?.let { startActivity(it) }   // No validation at all
        finish()
    }
}

2. setResult() 数据泄露

// VULNERABLE — Returns internal data to the caller
public class LeakyActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent next = getIntent().getParcelableExtra("next");
        startActivityForResult(next, 1001);
    }

    @Override
    protected void onActivityResult(int req, int res, Intent data) {
        super.onActivityResult(req, res, data);
        // Forwards internal data straight back to the (attacker) caller
        setResult(res, data);
        finish();
    }
}

3. 深度链接利用

// VULNERABLE — Deep link handler forwards without checking scheme/host
class DeepLinkActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val uri = intent.data
        val redirect = Intent(Intent.ACTION_VIEW, uri)
        // Attacker can craft: myapp://redirect?url=intent://...
        startActivity(redirect)
    }
}

攻击场景

场景 1 — 访问未导出的组件

攻击者构造一个针对受害方未导出的 InternalSettingsActivity 的 intent:

Intent inner = new Intent();
inner.setComponent(new ComponentName(
    "com.victim.app",
    "com.victim.app.InternalSettingsActivity"
));

Intent outer = new Intent();
outer.setComponent(new ComponentName(
    "com.victim.app",
    "com.victim.app.RouterActivity"  // exported proxy
));
outer.putExtra("target", inner);
startActivity(outer);

场景 2 — Content Provider 数据窃取

攻击者利用 URI 权限授权来读取受害方私有的 content provider:

val inner = Intent().apply {
    data = Uri.parse("content://com.victim.app.provider/private_data")
    flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}

val outer = Intent().apply {
    component = ComponentName("com.victim.app", "com.victim.app.ProxyActivity")
    putExtra("next_intent", inner)
}
startActivityForResult(outer, 0)
// onActivityResult receives the private data

场景 3 — 身份验证 / 会话劫持

如果受害应用有一个已导出的 activity,它会从内部登录流程转发结果,那么攻击者就能截获通过 setResult() 返回的身份验证令牌。

场景 4 — 深度链接串联

攻击者跨多个应用串联多个深度链接,将每个应用作为跳板,最终触及某个高价值目标中的受保护组件。

PendingIntent 重定向

经典的 Intent 重定向利用的是:某个应用以自身身份转发攻击者提供的 Intent;而 PendingIntent 重定向则将攻击反转过来:受害应用被诱使交出一个 PendingIntent,而攻击者可将其武器化。由于 PendingIntent 会以创建者的 UID 和权限来执行其所包装的 Intent,获得可变(mutable)或空 PendingIntent 的攻击者实际上就借用了受害应用的身份。

理解 PendingIntent 的安全模型

PendingIntent 是一个能力令牌(capability token),而非数据对象。其中的 Intent、标志位和创建者 UID 都存放在 system_server 中;应用只持有一个 binder 句柄。

  • 创建者(Creator):调用 PendingIntent.getActivity() → system_server 存储该记录并返回一个句柄。
  • 接收者(Recipient):通过 IPC(Intent extra、通知等)接收该句柄。
  • 分派(Dispatch):当调用 .send() 时,system_server 查找该记录并以创建者的 UID(而非发送者的 UID)触发所包装的 Intent。

创建者 UID 和 Intent 从不离开 system_server——接收者只持有一个不透明的 binder 句柄

这种设计使得 PendingIntent 默认可以安全地四处传递——这也是它被推荐作为经典 Intent 重定向缓解措施的原因。只有当创建者把这个能力令牌当作无害的数据来对待时,漏洞才会出现。

脆弱模式

当受害应用出现以下情况时,就会发生 PendingIntent 重定向:

  1. 创建一个带 FLAG_MUTABLE 的 PendingIntent(或在 Android 12 之前的目标上省略标志位,当时 mutable 是默认值),并且
  2. 包装一个隐式 Intent(未设置明确的组件),并且
  3. 将该 PendingIntent 暴露给攻击者——通过将其放入广播、通知操作、已导出 service 的响应,或返回给调用方的 Intent 中。

接收到可变 PendingIntent 的攻击者可以调用 .send(Context, int, Intent fillInIntent),并提供一个 fillInIntent 来补上缺失的组件、action、data 或 extra。系统会按照 Intent.fillIn() 中的规则将 fillIn 字段合并进原始 Intent,并以创建者的 UID 分派结果。

实际影响包括:

  • 启动受害方的未导出 activity、service 或 provider。
  • 通过 fillIn 授予 FLAG_GRANT_READ_URI_PERMISSION,从而读取或写入受害方私有的 content:// URI。
  • 以受害方身份向那些信任受害方签名或包名的接收者发送广播。
  • 触发特权内部流程(账户管理、设置更改、应用内购买回调)。

一个具体示例

// Vulnerable code inside VictimApp
Intent intent = new Intent();                         // implicit — no component
PendingIntent pi = PendingIntent.getActivity(
    this, 0, intent,
    PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

Intent deliver = new Intent("com.victim.HAND_OUT_TOKEN");
deliver.putExtra("token", pi);
sendBroadcast(deliver);                               // attacker receives pi

攻击者的接收器获取该 PendingIntent 并加以利用:

// Inside AttackerApp
PendingIntent pi = intent.getParcelableExtra("token", PendingIntent.class);

Intent fillIn = new Intent();
fillIn.setClassName("com.victim", "com.victim.internal.AdminActivity");
fillIn.putExtra("cmd", "wipe");

pi.send(context, 0, fillIn);      // launches VictimApp's internal activity as VictimApp

尽管 AdminActivity 并未导出,但启动仍然成功,因为系统是以 VictimApp 的 UID 来执行它的。

为何这与经典 Intent 重定向不同

方面 经典 Intent 重定向 PendingIntent 重定向
攻击者提供的内容 向受害方提供一个原始 Intent 什么都不提供——攻击者从受害方接收一个令牌
受害方的角色 提取一个 Intent 并分派它 交出一个包装了隐式、可变 Intent 的 PendingIntent
Intent 运行所依据的身份 受害方(混淆代理,confused deputy) 受害方(能力委派,capability delegation)
主要缓解措施 校验/过滤被转发的 Intent;改用 PendingIntent 使用 FLAG_IMMUTABLE;使用显式 Intent

造成的危害是一样的——代码以受害方身份执行——但攻击的形态是反转的。经典重定向需要一条输入路径,受害方在其中接受攻击者数据;而 PendingIntent 重定向需要一条输出路径,受害方在其中泄露了一项能力。

缓解策略

1. 使用 resolveActivity() 进行校验

在转发任何 intent 之前,先验证它解析到一个安全的、预期的组件:

val forwarded = intent.getParcelableExtra<Intent>("next")
forwarded?.let {
    val resolved = it.resolveActivity(packageManager)
    if (resolved != null && resolved.packageName == packageName) {
        // GOOD — only allow intents targeting our own package
        startActivity(it)
    }
}

2. 组件允许清单

维护一组明确许可的目标组件:

private val ALLOWED_TARGETS = setOf(
    "com.myapp.HomeActivity",
    "com.myapp.SettingsActivity"
)

fun safeForward(intent: Intent) {
    val target = intent.getParcelableExtra<Intent>("target") ?: return
    val comp = target.component?.className
    if (comp in ALLOWED_TARGETS) {
        startActivity(target)
    }
}

3. 使用 IntentSanitizer(AndroidX)

Jetpack 的 IntentSanitizer API 提供了一套声明式、构建器风格的 API,用于剥离危险字段:

val sanitizer = IntentSanitizer.Builder()
    .allowComponent(ComponentName(this, HomeActivity::class.java))
    .allowAction(Intent.ACTION_VIEW)
    .allowDataWithAuthority("myapp.example.com")
    .allowExtra("safe_key", String::class.java)
    .build()

val clean = sanitizer.sanitizeByFiltering(untrustedIntent)
startActivity(clean)

4. 限制已导出的组件

检查您的 AndroidManifest.xml,确保只有在确实必要时才导出组件:

<!-- GOOD — not exported; cannot be reached externally -->
<activity
    android:name=".InternalSettingsActivity"
    android:exported="false" />

<!-- If exported is required, protect with a permission -->
<activity
    android:name=".RouterActivity"
    android:exported="true"
    android:permission="com.myapp.permission.INTERNAL" />

5. 不可变的 PendingIntent

除非 PendingIntent 确实需要可变,否则始终使用 FLAG_IMMUTABLE:

val pi = PendingIntent.getActivity(
    context, 0, intent,
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)

6. 剥离危险的 Intent 标志位

在转发之前,移除可能授予 URI 权限的标志位:

fun stripDangerousFlags(intent: Intent): Intent {
    intent.removeFlags(
        Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
        Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
    )
    return intent
}

常见误解

误解 实际情况
“设置 android:exported=false 就能让我的组件安全。” 如果某个已导出的代理 activity 向它转发 intent,该组件实际上仍可被触及。
“我校验了 intent 的 action,所以我受到保护。” 攻击者控制着所有字段——组件、data URI、extra、标志位。仅校验 action 是不够的。
“目标组件上的权限检查会拦住攻击者。” 被转发的 intent 以受害应用的身份运行,而它本就持有所需的权限。
“只有 startActivity() 才脆弱。” sendBroadcast()、startService()、bindService() 和 startActivityForResult() 都可能受影响。
“深度链接是安全的,因为它们只打开网页 URL。” intent:// scheme 允许从一个 URI 构造任意 intent,从而绕过对 URL 的常规假设。

真实世界的影响

  • TikTok(2022): 研究人员演示了一条 intent 重定向链如何通过触及处理身份验证令牌的未导出 activity 来劫持用户账户。该发现获得了可观的漏洞赏金。
  • Google 安全公告: 多份 Android 安全公告都处理过系统级组件中的 intent 重定向缺陷,这凸显出即便是第一方代码也并非免疫。
  • 漏洞赏金趋势: 在 HackerOne 和 Bugcrowd 等平台上,intent 重定向一直位列 Android 漏洞类别的前列,其赏金也反映出它的高影响。

结论:纵深防御

没有任何单一的修复能够消除 intent 重定向风险。分层的方法必不可少:

  1. 尽量减少导出——只导出那些确实需要外部访问的组件。
  2. 校验所有被转发的 intent——使用 resolveActivity()、组件允许清单或 IntentSanitizer。
  3. 剥离危险的标志位——在转发前移除 URI 授权标志位。
  4. 使用不可变的 PendingIntent——默认采用 FLAG_IMMUTABLE。
  5. 保护敏感结果——绝不通过 setResult() 向未经验证的调用方返回内部数据。
  6. 集成静态分析——Android Lint 和 Semgrep 等工具可以在发布前于 CI/CD 中捕获配置错误。

通过将每一个从外部接收的 intent 都视为不受信任的输入,并一致地应用这些控制措施,Android 开发者就能有效地将 intent 重定向作为一种攻击向量予以消除。