หมวดหมู่ OWASP: MASVS-PLATFORM: การโต้ตอบกับแพลตฟอร์ม
ภาพรวม
ตามเอกสารประกอบ ContentResolver คือ "คลาสที่ให้สิทธิ์เข้าถึงโมเดลเนื้อหาแก่แอปพลิเคชัน" ContentResolvers เปิดเผยเมธอดเพื่อโต้ตอบ ดึงข้อมูล หรือแก้ไขเนื้อหาที่มาจาก:
- `ContentResolver` มีเมธอดสำหรับการโต้ตอบ ดึง หรือแก้ไขเนื้อหาที่มาจากแอปที่ติดตั้ง (
content://URI Scheme) - ระบบไฟล์ (
file://URI Scheme) - API ที่รองรับตามที่ Android จัดหาให้ (
android.resource://URI Scheme)
สรุปได้ว่าช่องโหว่ที่เกี่ยวข้องกับ ContentResolver อยู่ในคลาส Confused Deputy เนื่องจากผู้โจมตีสามารถใช้สิทธิ์ของแอปพลิเคชันที่มีช่องโหว่เพื่อเข้าถึงเนื้อหาที่ได้รับการปกป้อง
ความเสี่ยง: การละเมิดตาม URI `file:// ` ที่ไม่น่าเชื่อถือ
การละเมิด ContentResolver โดยใช้ช่องโหว่ URI file:// จะใช้ประโยชน์จากความสามารถของ ContentResolver ในการแสดงตัวอธิบายไฟล์ที่อธิบายโดย URI ช่องโหว่นี้ส่งผลต่อฟังก์ชันต่างๆ เช่น openFile(), openFileDescriptor(), openInputStream(), openOutputStream(), หรือ openAssetFileDescriptor() จาก ContentResolver API ผู้โจมตีสามารถใช้ประโยชน์จากช่องโหว่นี้ด้วย URI file:// ที่ควบคุมโดยผู้โจมตีทั้งหมดหรือบางส่วนเพื่อบังคับให้แอปพลิเคชันเข้าถึงไฟล์ที่ไม่ได้มีไว้ให้เข้าถึงได้ เช่น ฐานข้อมูลภายในหรือค่ากำหนดที่แชร์
สถานการณ์การโจมตีที่เป็นไปได้วิธีหนึ่งคือการสร้างแกลเลอรีหรือเครื่องมือเลือกไฟล์ที่เป็นอันตราย ซึ่งเมื่อแอปที่มีช่องโหว่ใช้แล้ว จะแสดง URI ที่เป็นอันตราย
การโจมตีนี้มีหลายรูปแบบ ดังนี้
- URI
file://ที่ควบคุมโดยผู้โจมตีทั้งหมดซึ่งชี้ไปยังไฟล์ภายในของแอป - URI
file://บางส่วนควบคุมโดยผู้โจมตี ทำให้เกิดการข้ามเส้นทางได้ง่าย - URI
file://ที่กำหนดเป้าหมายเป็นลิงก์สัญลักษณ์ (Symlink) ที่ควบคุมโดยผู้โจมตีซึ่งชี้ไปยังไฟล์ภายในของแอป - คล้ายกับรูปแบบก่อนหน้า แต่ในกรณีนี้ผู้โจมตีจะสลับเป้าหมายของ 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 ขาเข้า เช่น การใช้รายการที่อนุญาตของ Authority ที่คาดไว้ถือเป็นแนวทางปฏิบัติแนะนำ
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;
}
หากการใช้ผู้ให้บริการเนื้อหารายอื่นไม่จำเป็นต้องมีการให้สิทธิ์ เช่น เมื่อแอปอนุญาตให้แอปทั้งหมดจากระบบนิเวศเข้าถึงข้อมูลทั้งหมด ให้ห้ามการใช้ Authority เหล่านี้อย่างชัดเจน