รีโซลเวอร์เนื้อหา

หมวดหมู่ OWASP: MASVS-PLATFORM: การโต้ตอบกับแพลตฟอร์ม

ภาพรวม

ตามเอกสารประกอบ ContentResolver คือ"คลาสที่ให้สิทธิ์เข้าถึงโมเดลเนื้อหาแก่แอปพลิเคชัน" ContentResolver จะแสดงเมธอดเพื่อโต้ตอบ ดึงข้อมูล หรือแก้ไขเนื้อหาที่ได้จากแหล่งที่มาต่อไปนี้

  • แอปที่ติดตั้ง (รูปแบบ URI content://)
  • ระบบไฟล์ (รูปแบบ URI file://)
  • การรองรับ API ที่ Android มีให้ (รูปแบบ URI android.resource://)

โดยสรุปแล้ว ช่องโหว่ที่เกี่ยวข้องกับ ContentResolver อยู่ในคลาส confused deputy เนื่องจากผู้โจมตีสามารถใช้สิทธิ์ของแอปพลิเคชันที่เปราะบางเพื่อเข้าถึงเนื้อหาที่ได้รับการคุ้มครอง

ความเสี่ยง: การละเมิดโดยอิงตาม URI file:// ที่ไม่น่าเชื่อถือ

การละเมิด ContentResolver โดยใช้ช่องโหว่ URI ของ file:// จะใช้ประโยชน์จากความสามารถของ ContentResolver ในการแสดงผลตัวระบุไฟล์ที่อธิบายโดย URI ช่องโหว่นี้ส่งผลกระทบต่อฟังก์ชันต่างๆ เช่น openFile(), openFileDescriptor(), openInputStream(), openOutputStream() หรือ openAssetFileDescriptor() จาก ContentResolver API ช่องโหว่นี้อาจถูกละเมิดด้วย file:// URI ที่ควบคุมโดยผู้โจมตีทั้งหมดหรือบางส่วนเพื่อบังคับให้แอปพลิเคชันเข้าถึงไฟล์ที่ไม่ได้ตั้งใจให้เข้าถึง เช่น ฐานข้อมูลภายในหรือค่ากําหนดที่ใช้ร่วมกัน

สถานการณ์การโจมตีที่เป็นไปได้อย่างหนึ่งคือการสร้างแกลเลอรีหรือเครื่องมือเลือกไฟล์ที่เป็นอันตราย ซึ่งจะแสดงผล URI ที่เป็นอันตรายเมื่อแอปที่มีช่องโหว่นำมาใช้

การโจมตีนี้มีรูปแบบต่างๆ ดังนี้

  • file:// URI ที่ควบคุมโดยผู้โจมตีโดยสมบูรณ์ซึ่งชี้ไปยังไฟล์ภายในของแอป
  • URI ของ file:// บางส่วนอยู่ภายใต้การควบคุมของผู้โจมตี จึงมีแนวโน้มที่จะเกิดการข้ามเส้นทาง
  • file:// URI ที่กําหนดเป้าหมายไปยัง Symbolic Link (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.
}


ความเสี่ยง: การละเมิดตาม URI ของ content:// ที่ไม่เชื่อถือ

การละเมิด ContentResolver โดยใช้ช่องโหว่ URI ของ content:// เกิดขึ้นเมื่อมีการส่ง URI ที่ผู้โจมตีควบคุมทั้งหมดหรือบางส่วนไปยัง ContentResolver API เพื่อดําเนินการกับเนื้อหาที่ไม่ได้ตั้งใจให้เข้าถึง

การโจมตีนี้มี 2 สถานการณ์หลักๆ ดังนี้

  • แอปทำงานกับเนื้อหาภายในของตนเอง ตัวอย่างเช่น หลังจากได้รับ 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;
}

หากการใช้ผู้ให้บริการเนื้อหารายอื่นไม่จําเป็นต้องได้รับสิทธิ์ เช่น เมื่อแอปอนุญาตให้แอปทั้งหมดจากระบบนิเวศเข้าถึงข้อมูลทั้งหมด ให้ห้ามการใช้สิทธิ์เหล่านี้อย่างชัดเจน