콘텐츠 리졸버

OWASP 카테고리: MASVS-PLATFORM: 플랫폼 상호작용

개요

문서에 따르면 ContentResolver'애플리케이션에 콘텐츠 모델에 대한 액세스 권한을 제공하는 클래스'입니다. ContentResolver는 다음에서 제공하는 콘텐츠와 상호작용하거나 이를 가져오거나 수정하는 메서드를 노출합니다.

  • 설치된 앱(content:// URI 스킴)
  • 파일 시스템(file:// URI 스킴)
  • Android에서 제공하는 지원 API(android.resource:// URI 스킴)

요약하자면, ContentResolver와 관련된 취약점은 공격자가 취약한 애플리케이션의 권한을 사용하여 보호된 콘텐츠에 액세스할 수 있으므로 혼동된 대리인 클래스에 속합니다.

위험: 신뢰할 수 없는 file:// URI 기반 악용

file:// URI 취약점을 사용하는 ContentResolver 악용은 URI에서 설명하는 파일 설명자를 반환하는 ContentResolver의 기능을 악용합니다. 이 취약점은 ContentResolver API 함수(openFile(), openFileDescriptor(), openInputStream(), openOutputStream(), openAssetFileDescriptor() 등)에 영향을 줍니다. 이 취약점은 공격자가 완전히 또는 부분적으로 제어하는 file:// URI를 사용하여 애플리케이션이 액세스되어서는 안 되는 파일(예: 내부 데이터베이스, 공유 환경설정 등)에 액세스하도록 강제함으로써 악용될 수 있습니다.

가능한 공격 시나리오 중 하나로, 취약한 앱에 의해 사용될 경우 악성 URI를 반환하는 악성 갤러리 또는 파일 선택 도구를 만드는 것을 들 수 있습니다.

이 공격에는 몇 가지 변형이 있습니다.

  • 앱의 내부 파일을 가리키는 file:// URI를 공격자가 완전히 제어함
  • file:// URI의 일부를 공격자가 제어하여 경로 순회에 취약해짐
  • 앱의 내부 파일을 가리키는 심볼릭 링크를 공격자가 제어하고 이를 file:// URI가 타겟팅함
  • 이전 변형과 비슷하나 이 경우에는 공격자가 심볼릭 링크 타겟을 정상 타겟에서 앱의 내부 파일로 반복적으로 교체합니다. 목표는 잠재적인 보안 검사와 파일 경로 사용 간의 경합 상태를 악용하는 것입니다.

영향

이 취약점을 악용할 때의 영향은 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;
}

다른 콘텐츠 제공자를 사용하는 데 권한 부여가 필요하지 않다면(예: 생태계의 모든 앱이 모든 데이터에 액세스할 수 있도록 앱이 허용하는 경우) 이러한 권한의 사용을 명시적으로 금지합니다.