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