mirror of
https://github.com/Mr-xn/Penetration_Testing_POC.git
synced 2026-10-24 08:54:28 +00:00
251 lines
9.4 KiB
Markdown
251 lines
9.4 KiB
Markdown
# Android移动安全第十五章_FragmentInjection
|
||||
|
|
> QIANXIN Team
|
|||
|
|
> 来源:https://forum.butian.net/share/4946
|
|||
|
|
|
|||
|
|
> 系列目录:
|
|||
|
|
>
|
|||
|
|
> 1. Android 组件导出安全
|
|||
|
|
> 2. Android Intent 安全
|
|||
|
|
> 3. Android Binder 服务安全
|
|||
|
|
> 4. Android ContentProvider 安全
|
|||
|
|
> 5. Android WebView 安全
|
|||
|
|
> 6. Android UI 欺骗与钓鱼
|
|||
|
|
> 7. Android Deep Link 安全
|
|||
|
|
> 8. Android 广播安全
|
|||
|
|
> 9. Android PendingIntent 安全
|
|||
|
|
> 10. Android 系统设置安全
|
|||
|
|
> 11. Android SSRF 与网络安全
|
|||
|
|
> 12. Android 加密与数据存储安全
|
|||
|
|
> 13. Android 认证与证书校验
|
|||
|
|
> 14. Android Zip Slip 路径遍历
|
|||
|
|
> 15. Android Fragment Injection(本章)
|
|||
|
|
> 16. Android SELinux 与沙箱机制
|
|||
|
|
|
|||
|
|
* * *
|
|||
|
|
|
|||
|
|
## 1\. 前言
|
|||
|
|
|
|||
|
|
Fragment 是 Android 3.0(API 11)引入的 UI 组件。一个 Activity 可以包含多个 Fragment,每个 Fragment 管理自己的布局和生命周期。典型的使用场景是设置页面——左侧是分类列表,右侧根据选择加载不同的 Fragment 显示对应的设置项。
|
|||
|
|
|
|||
|
|
Android 提供了 PreferenceActivity 来简化设置页面的开发。PreferenceActivity 支持通过 Intent 的 extra 参数指定要加载的 Fragment 类名:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
Intent intent = new Intent(this, SettingsActivity.class);
|
|||
|
|
intent.putExtra(":android:show_fragment", "com.example.NetworkSettingsFragment");
|
|||
|
|
startActivity(intent);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
PreferenceActivity 收到这个 Intent 后,会用反射实例化指定的 Fragment 并显示。问题在于,如果 SettingsActivity 是导出的(设置页面通常需要导出,因为系统设置、快捷方式等需要跳转到它),外部 App 也可以通过这个参数指定任意 Fragment 类名。
|
|||
|
|
|
|||
|
|
* * *
|
|||
|
|
|
|||
|
|
## 2\. 漏洞原理
|
|||
|
|
|
|||
|
|
### 2.1 PreferenceActivity 的 Fragment 加载机制
|
|||
|
|
|
|||
|
|
PreferenceActivity 在 `onCreate()` 中检查 Intent 是否包含 `:android:show_fragment` 这个 extra。如果有,就调用 `Fragment.instantiate()` 用反射创建该 Fragment 实例并加载到界面中:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
|
|||
|
|
String fragmentName = intent.getStringExtra(":android:show_fragment");
|
|||
|
|
if (fragmentName != null) {
|
|||
|
|
Bundle args = intent.getBundleExtra(":android:show_fragment_args");
|
|||
|
|
Fragment f = Fragment.instantiate(this, fragmentName, args);
|
|||
|
|
getFragmentManager().beginTransaction()
|
|||
|
|
.replace(android.R.id.content, f)
|
|||
|
|
.commit();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`Fragment.instantiate()` 内部用 `Class.forName(fragmentName)` 加载类,然后调用无参构造函数创建实例。只要类名存在于 App 的 ClassLoader 中,就能被实例化。
|
|||
|
|
|
|||
|
|
### 2.2 攻击效果
|
|||
|
|
|
|||
|
|
App 内部可能有一些 Fragment 不打算对外暴露,比如:
|
|||
|
|
|
|||
|
|
- 调试信息 Fragment(显示内部日志、设备信息)
|
|||
|
|
- 账户管理 Fragment(修改密码、绑定手机号)
|
|||
|
|
- 高级设置 Fragment(开发者选项、隐藏功能开关)
|
|||
|
|
- 数据导出 Fragment(导出用户数据)
|
|||
|
|
|
|||
|
|
这些 Fragment 本身没有导出的概念(Fragment 不是四大组件,不在 Manifest 中声明),它们的访问控制完全依赖于宿主 Activity。如果宿主 Activity 是导出的 PreferenceActivity,且没有校验 Fragment 类名,攻击者就能加载这些内部 Fragment。
|
|||
|
|
|
|||
|
|
攻击命令:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
adb shell am start -n com.target.app/.SettingsActivity \
|
|||
|
|
--es ":android:show_fragment" "com.target.app.internal.DebugFragment"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.3 Fragment 参数注入
|
|||
|
|
|
|||
|
|
除了 `:android:show_fragment`,PreferenceActivity 还支持 `:android:show_fragment_args` 参数,用于向 Fragment 传递 Bundle 参数。Fragment 通过 `getArguments()` 获取这些参数:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public class DataExportFragment extends Fragment {
|
|||
|
|
@Override
|
|||
|
|
public void onCreate(Bundle savedInstanceState) {
|
|||
|
|
super.onCreate(savedInstanceState);
|
|||
|
|
Bundle args = getArguments();
|
|||
|
|
if (args != null) {
|
|||
|
|
String exportPath = args.getString("export_path");
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
攻击者不仅能指定加载哪个 Fragment,还能控制传给 Fragment 的参数。
|
|||
|
|
|
|||
|
|
* * *
|
|||
|
|
|
|||
|
|
## 3\. isValidFragment() 校验
|
|||
|
|
|
|||
|
|
### 3.1 Android 4.4 的修复
|
|||
|
|
|
|||
|
|
这个漏洞在 2013 年被公开(CVE-2014-1710 是其中一个相关编号)。Android 4.4(API 19)在 PreferenceActivity 中添加了 `isValidFragment()` 方法作为修复:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
|
|||
|
|
protected boolean isValidFragment(String fragmentName) {
|
|||
|
|
|
|||
|
|
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.KITKAT) {
|
|||
|
|
throw new RuntimeException(
|
|||
|
|
"Subclasses of PreferenceActivity must override isValidFragment(String)"
|
|||
|
|
+ " to verify that the Fragment class is valid!"
|
|||
|
|
+ " " + this.getClass().getName()
|
|||
|
|
+ " has not checked if fragment " + fragmentName + " is valid.");
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
PreferenceActivity 在加载 Fragment 之前会调用 `isValidFragment(fragmentName)`。如果 App 的 targetSdk >= 19,默认实现直接抛异常,强制开发者重写这个方法来校验 Fragment 类名。
|
|||
|
|
|
|||
|
|
### 3.2 正确的重写方式
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public class SettingsActivity extends PreferenceActivity {
|
|||
|
|
@Override
|
|||
|
|
protected boolean isValidFragment(String fragmentName) {
|
|||
|
|
return GeneralSettingsFragment.class.getName().equals(fragmentName)
|
|||
|
|
|| NetworkSettingsFragment.class.getName().equals(fragmentName)
|
|||
|
|
|| DisplaySettingsFragment.class.getName().equals(fragmentName);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
白名单方式——只允许加载预期的 Fragment 类。
|
|||
|
|
|
|||
|
|
### 3.3 常见的错误实现
|
|||
|
|
|
|||
|
|
实际审计中,经常看到开发者为了消除异常而直接返回 true:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Override
|
|||
|
|
protected boolean isValidFragment(String fragmentName) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
或者用包名前缀做粗粒度校验:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Override
|
|||
|
|
protected boolean isValidFragment(String fragmentName) {
|
|||
|
|
return fragmentName.startsWith("com.example.settings.");
|
|||
|
|
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这两种写法都没有真正解决问题。
|
|||
|
|
|
|||
|
|
* * *
|
|||
|
|
|
|||
|
|
## 4\. 不依赖 PreferenceActivity 的 Fragment Injection
|
|||
|
|
|
|||
|
|
### 4.1 自定义的 Fragment 加载逻辑
|
|||
|
|
|
|||
|
|
不只是 PreferenceActivity,任何根据外部输入动态加载 Fragment 的代码都可能存在 Fragment Injection:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
|
|||
|
|
public class ContainerActivity extends Activity {
|
|||
|
|
@Override
|
|||
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|||
|
|
super.onCreate(savedInstanceState);
|
|||
|
|
setContentView(R.layout.activity_container);
|
|||
|
|
|
|||
|
|
String fragmentClass = getIntent().getStringExtra("fragment_class");
|
|||
|
|
if (fragmentClass != null) {
|
|||
|
|
Fragment f = Fragment.instantiate(this, fragmentClass);
|
|||
|
|
getFragmentManager().beginTransaction()
|
|||
|
|
.replace(R.id.container, f)
|
|||
|
|
.commit();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
如果 ContainerActivity 是导出的,攻击者可以通过 `fragment_class` 参数加载任意 Fragment。
|
|||
|
|
|
|||
|
|
### 4.2 通过 Deep Link 触发
|
|||
|
|
|
|||
|
|
第七章讲过 Deep Link 的安全问题。如果 App 的 Deep Link 处理逻辑中包含 Fragment 加载:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
|
|||
|
|
Uri uri = getIntent().getData();
|
|||
|
|
String fragmentName = uri.getQueryParameter("fragment");
|
|||
|
|
if (fragmentName != null) {
|
|||
|
|
Fragment f = Fragment.instantiate(this, fragmentName);
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
攻击者可以构造恶意链接诱导用户点击,触发 Fragment Injection。
|
|||
|
|
|
|||
|
|
### 4.3 AndroidX Navigation 组件
|
|||
|
|
|
|||
|
|
使用 AndroidX Navigation 组件的 App,如果 NavGraph 中的 deep link 配置不当,也可能导致类似问题。Navigation 组件根据 URI 匹配 destination(目标 Fragment),如果 URI 模式过于宽泛:
|
|||
|
|
|
|||
|
|
```xml
|
|||
|
|
<fragment
|
|||
|
|
android:id="@+id/settingsFragment"
|
|||
|
|
android:name="com.example.SettingsFragment">
|
|||
|
|
<deepLink app:uri="myapp://fragment/{name}" />
|
|||
|
|
</fragment>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这里 `{name}` 是路径参数,传给 Fragment 作为 argument。虽然 Navigation 组件不会根据参数动态切换 Fragment 类(destination 是固定的),但参数值仍然是攻击者可控的,可能影响 Fragment 内部逻辑。
|
|||
|
|
|
|||
|
|
* * *
|
|||
|
|
|
|||
|
|
## 5\. 版本演进
|
|||
|
|
|
|||
|
|
| 版本 | 变化 |
|
|||
|
|
| --- | --- |
|
|||
|
|
| Android 3.0 (API 11) | 引入 Fragment 和 PreferenceActivity 的 Fragment 加载机制 |
|
|||
|
|
| Android 4.4 (API 19) | 添加 isValidFragment(),targetSdk >= 19 时默认抛异常 |
|
|||
|
|
| Android 9.0 (API 28) | PreferenceActivity 被标记为 deprecated,推荐使用 AndroidX Preference |
|
|||
|
|
| Android 13+ | AndroidX PreferenceFragmentCompat 成为主流,不存在 isValidFragment 问题 |
|
|||
|
|
|
|||
|
|
Android 4.4 的修复覆盖了 PreferenceActivity 的场景,但自定义的 Fragment 加载逻辑不受此保护。随着 AndroidX Preference 库的普及,PreferenceActivity 的使用在减少,但老应用和系统预装 App 中仍然常见。
|
|||
|
|
|
|||
|
|
AndroidX 的 PreferenceFragmentCompat 不使用 `:android:show_fragment` 机制,Fragment 的加载由 NavGraph 或代码显式控制,从架构上避免了这个问题。
|
|||
|
|
|
|||
|
|
* * *
|
|||
|
|
|
|||
|
|
## 6\. 总结
|
|||
|
|
|
|||
|
|
Fragment Injection 的本质是信任了外部输入的类名。PreferenceActivity 的 `:android:show_fragment` 参数是最经典的入口,但任何根据外部输入动态实例化 Fragment 的代码都有同样的风险。
|
|||
|
|
|
|||
|
|
回顾一下:
|
|||
|
|
|
|||
|
|
- PreferenceActivity 根据 Intent extra 中的类名用反射加载 Fragment
|
|||
|
|
- Android 4.4 添加了 isValidFragment() 校验,但开发者可能直接返回 true
|
|||
|
|
- 自定义的 Fragment 加载逻辑不受 isValidFragment() 保护
|
|||
|
|
- Fragment 没有独立的导出控制,访问控制完全依赖宿主 Activity
|
|||
|
|
|
|||
|
|
下一章讲 Android SELinux 与沙箱机制。SELinux 是 Linux 内核的强制访问控制模块,Android 从 4.3 开始引入,用于限制进程的权限范围。即使 App 获得了 root 权限,SELinux 策略仍然可以阻止它访问特定资源。
|