内容解析器

OWASP 类别: MASVS-PLATFORM:平台交互

概览

根据文档ContentResolver 是一个“为应用提供对内容模型访问权限的类”。ContentResolver 公开用于与以下来源提供的内容进行交互、获取或修改的方法

  • 已安装的应用 (content:// URI scheme)
  • 文件系统 (file:// URI scheme)
  • Android 提供的支持 API (android.resource:// URI scheme)。

总而言之,与 ContentResolver 相关的漏洞属于困惑的代理人类别,因为攻击者可以利用有漏洞应用的权限来访问受保护的内容。

风险:基于不可信的 file:// URI 进行滥用

滥用使用 file:// URI 漏洞的 ContentResolver 利用了 ContentResolver 返回 URI 所描述的文件描述符的功能。此漏洞影响 ContentResolver API 中的函数,例如 openFile()openFileDescriptor()openInputStream()openOutputStream()openAssetFileDescriptor()。该漏洞可以通过完全或部分由攻击者控制的 file:// URI 进行滥用,强制应用访问本来无权访问的文件,例如内部数据库或共享偏好设置。

一种可能的攻击场景是创建一个恶意的图库或文件选择器,当被有漏洞的应用使用时,会返回一个恶意的 URI。

此攻击有几种变体

  • 完全由攻击者控制的 file:// URI,指向应用的内部文件
  • file:// URI 的一部分由攻击者控制,使其容易受到路径遍历攻击
  • file:// URI 定位到由攻击者控制的符号链接 (symlink),该链接指向应用的内部文件
  • 类似于前述变体,但攻击者在此处反复将符号链接目标从合法目标切换到应用的内部文件。目标是利用潜在的安全检查和文件路径使用之间的竞态条件

影响

利用此漏洞的影响取决于 ContentResolver 的用途。在许多情况下,这可能导致应用受保护数据被泄露或被未经授权的各方修改。

缓解措施

要缓解此漏洞,请使用以下算法验证文件描述符。通过验证后,文件描述符可以安全使用。

Kotlin

fun isValidFile(ctx: Context, pfd: ParcelFileDescriptor, fileUri: Uri): Boolean {
    // Canonicalize to resolve symlinks and path traversals.
    val fdCanonical = File(fileUri.path!!).canonicalPath

    val pfdStat: StructStat = Os.fstat(pfd.fileDescriptor)

    // Lstat doesn't follow the symlink.
    val canonicalFileStat: StructStat = Os.lstat(fdCanonical)

    // Since we canonicalized (followed the links) the path already,
    // the path shouldn't point to symlink unless it was changed in the
    // meantime.
    if (OsConstants.S_ISLNK(canonicalFileStat.st_mode)) {
        return false
    }

    val sameFile =
        pfdStat.st_dev == canonicalFileStat.st_dev &&
        pfdStat.st_ino == canonicalFileStat.st_ino

    if (!sameFile) {
        return false
    }

    return !isBlockedPath(ctx, fdCanonical)
}

fun isBlockedPath(ctx: Context, fdCanonical: String): Boolean {
    // Paths that should rarely be exposed
    if (fdCanonical.startsWith("/proc/") ||
        fdCanonical.startsWith("/data/misc/")) {
        return true
    }

    // Implement logic to block desired directories. For example, specify
    // the entire app data/ directory to block all access.
}

Java

boolean isValidFile(Context ctx, ParcelFileDescriptor pfd, Uri fileUri) {
    // Canonicalize to resolve symlinks and path traversals
    String fdCanonical = new File(fileUri.getPath()).getCanonicalPath();

    StructStat pfdStat = Os.fstat(pfd.getFileDescriptor());

    // Lstat doesn't follow the symlink. 
    StructStat canonicalFileStat = Os.lstat(fdCanonical);

    // Since we canonicalized (followed the links) the path already, 
    // the path shouldn't point to symlink unless it was changed in the meantime
    if (OsConstants.S_ISLNK(canonicalFileStat.st_mode)) {
        return false;
    }

    boolean sameFile =
        pfdStat.stDev == canonicalFileStat.stDev && pfdStat.stIno == canonicalFileStat.stIno;

    if (!sameFile) {
        return false;
    }

    return !isBlockedPath(ctx, fdCanonical);
} 

boolean isBlockedPath(Context ctx, String fdCanonical) {
        
        // Paths that should rarely be exposed
        if (fdCanonical.startsWith("/proc/") || fdCanonical.startsWith("/data/misc/")) {
            return true;
        }

        // Implement logic to block desired directories. For example, specify
        // the entire app data/ directory to block all access.
}


风险:基于不可信的 content:// URI 进行滥用

滥用使用 content:// URI 漏洞的 ContentResolver 发生在将完全或部分由攻击者控制的 URI 传递给 ContentResolver API 以操作本来无权访问的内容时。

此攻击有两种主要场景

  • 应用操作自己的内部内容。例如:在从攻击者那里获取 URI 后,邮件应用附加的是其自身内部内容提供程序的数据,而不是外部照片。
  • 应用充当代理,然后代表攻击者访问另一个应用的数据。例如:邮件应用附加的是来自应用 X 的数据,这些数据受权限保护,正常情况下会阻止攻击者查看该特定附件。这些数据对于执行附件操作的应用是可用的,但最初并非如此,因此会将此内容中继给攻击者。

一种可能的攻击场景是创建一个恶意的图库或文件选择器,当被有漏洞的应用使用时,会返回一个恶意的 URI。

影响

利用此漏洞的影响取决于与 ContentResolver 关联的上下文。这可能导致应用受保护数据被泄露或受保护数据被未经授权的各方修改。

缓解措施

通用

验证传入的 URI。例如,使用期望的授权机构白名单被认为是良好的做法。

URI 针对属于有漏洞应用的未导出或受权限保护的内容提供程序

检查 URI 是否以您的应用为目标

Kotlin

fun belongsToCurrentApplication(ctx: Context, uri: Uri): Boolean {
    val authority: String = uri.authority.toString()
    val info: ProviderInfo =
        ctx.packageManager.resolveContentProvider(authority, 0)!!

    return ctx.packageName.equals(info.packageName)
}

Java

boolean belongsToCurrentApplication(Context ctx, Uri uri){
    String authority = uri.getAuthority();
    ProviderInfo info = ctx.getPackageManager().resolveContentProvider(authority, 0);

    return ctx.getPackageName().equals(info.packageName);
}

或目标提供程序是否已导出

Kotlin

fun isExported(ctx: Context, uri: Uri): Boolean {
    val authority = uri.authority.toString()
    val info: ProviderInfo =
            ctx.packageManager.resolveContentProvider(authority, 0)!!

    return info.exported
}

Java

boolean isExported(Context ctx, Uri uri){
    String authority = uri.getAuthority();
    ProviderInfo info = ctx.getPackageManager().resolveContentProvider(authority, 0);       

    return info.exported;
}

或者是否已授予对该 URI 的显式权限 - 此检查基于这样一个假设:如果已授予访问数据的显式权限,则该 URI 不是恶意的

Kotlin

// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
fun wasGrantedPermission(ctx: Context, uri: Uri?, grantFlag: Int): Boolean {
    val pid: Int = Process.myPid()
    val uid: Int = Process.myUid()
    return ctx.checkUriPermission(uri, pid, uid, grantFlag) ==
            PackageManager.PERMISSION_GRANTED
}

Java

// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
boolean wasGrantedPermission(Context ctx, Uri uri, int grantFlag){
    int pid = Process.myPid();
    int uid = Process.myUid();

    return ctx.checkUriPermission(uri, pid, uid, grantFlag) == PackageManager.PERMISSION_GRANTED;
}

URI 针对属于另一个应用且信任有漏洞应用的受权限保护的 ContentProvider。

此攻击与以下情况相关

  • 应用生态系统,其中应用定义和使用自定义权限或其他身份验证机制。
  • 权限代理攻击,其中攻击者滥用持有运行时权限(例如 READ_CONTACTS)的有漏洞应用,从系统提供程序检索数据。

测试是否已授予 URI 权限

Kotlin

// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
fun wasGrantedPermission(ctx: Context, uri: Uri?, grantFlag: Int): Boolean {
    val pid: Int = Process.myPid()
    val uid: Int = Process.myUid()
    return ctx.checkUriPermission(uri, pid, uid, grantFlag) ==
            PackageManager.PERMISSION_GRANTED
}

Java

// grantFlag is one of: FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
boolean wasGrantedPermission(Context ctx, Uri uri, int grantFlag){
    int pid = Process.myPid();
    int uid = Process.myUid();

    return ctx.checkUriPermission(uri, pid, uid, grantFlag) == PackageManager.PERMISSION_GRANTED;
}

如果使用其他内容提供程序不需要授予权限(例如当应用允许生态系统中的所有应用访问所有数据时),则明确禁止使用这些授权机构。