昨天出去玩了,没带电脑,所以今天补上昨天的。2023.09.17补2023.09.16
CVE-2022-20395 | A-221855295 | EoP | High | 11, 12, 12L, 13 |
---|
patch


分析
在MediaProvider的deleteIfAllowed
函数里存在路径穿越漏洞,会导致任意文件删除。patch是改为使用getCanonicalFile
和getCanonicalPath
,对路径进行正规化之后再使用。
- deleteIfAllowed
// packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
private void deleteIfAllowed(Uri uri, Bundle extras, String path) {
try {
final File file = new File(path); // 根据path获取File对象
checkAccess(uri, extras, file, true); // 检查是否可访问
deleteAndInvalidate(file); // 直接删除,可导致路径穿越
} catch (Exception e) {
Log.e(TAG, "Couldn't delete " + path, e);
}
}
- computeDataFromValues
// packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java
/**
* Compute {@link MediaColumns#DATA} from several scattered
* {@link MediaColumns} values. This method performs no enforcement of
* argument validity.
*/
public static void computeDataFromValues(@NonNull ContentValues values,
@NonNull File volumePath, boolean isForFuse) {
values.remove(MediaColumns.DATA);
final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
final String resolvedDisplayName;
// Pending file path shouldn't be rewritten for files inserted via filepath.
if (!isForFuse && getAsBoolean(values, MediaColumns.IS_PENDING, false)) {
final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
(System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
final String combinedString = String.format(
Locale.US, ".%s-%d-%s", FileUtils.PREFIX_PENDING, dateExpires, displayName);
// trim the file name to avoid ENAMETOOLONG error
// after trim the file, if the user unpending the file,
// the file name is not the original one
resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES);
} else if (getAsBoolean(values, MediaColumns.IS_TRASHED, false)) {
final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
(System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
final String combinedString = String.format(
Locale.US, ".%s-%d-%s", FileUtils.PREFIX_TRASHED, dateExpires, displayName);
// trim the file name to avoid ENAMETOOLONG error
// after trim the file, if the user untrashes the file,
// the file name is not the original one
resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES);
} else {
resolvedDisplayName = displayName;
}
// 这里没有判断values.getAsString(MediaColumns.RELATIVE_PATH)是否为空
final File filePath = buildPath(volumePath,
values.getAsString(MediaColumns.RELATIVE_PATH), resolvedDisplayName);
// 这里应该使用getCanonicalPath
values.put(MediaColumns.DATA, filePath.getAbsolutePath());
}
PoC
其实也就是test文件
+ @Test
+ public void testInsertionWithInvalidFilePath_throwsIllegalArgumentException() {
+ final ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.RELATIVE_PATH, "Android/media/com.example");
+ values.put(MediaStore.Images.Media.DISPLAY_NAME,
+ "./../../../../../../../../../../../data/media/test.txt");
+
+ IllegalArgumentException illegalArgumentException = Assert.assertThrows(
+ IllegalArgumentException.class, () -> sIsolatedResolver.insert(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+ values));
+
+ assertThat(illegalArgumentException).hasMessageThat().contains(
+ "Primary directory Android not allowed for content://media/external_primary/file;"
+ + " allowed directories are [Download, Documents]");
+ }
+
+ @Test
+ public void testUpdationWithInvalidFilePath_throwsIllegalArgumentException() {
+ final ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.RELATIVE_PATH, "Download");
+ values.put(MediaStore.Images.Media.DISPLAY_NAME, "test.txt");
+ Uri uri = sIsolatedResolver.insert(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+ values);
+
+ final ContentValues newValues = new ContentValues();
+ newValues.put(MediaStore.MediaColumns.DATA, "/storage/emulated/0/../../../data/media/");
+ IllegalArgumentException illegalArgumentException = Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> sIsolatedResolver.update(uri, newValues, null));
+
+ assertThat(illegalArgumentException).hasMessageThat().contains(
+ "Requested path /data/media doesn't appear under [/storage/emulated/0]");
+ }
+
@@ -63,6 +63,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -1200,4 +1201,27 @@
assertTrue(values.containsKey(MediaColumns.BUCKET_DISPLAY_NAME));
assertNull(values.get(MediaColumns.BUCKET_DISPLAY_NAME));
}
+
+ @Test
+ public void testComputeDataFromValuesForValidPath_success() {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.RELATIVE_PATH, "Android/media/com.example");
+ values.put(MediaColumns.DISPLAY_NAME, "./../../abc.txt");
+
+ FileUtils.computeDataFromValues(values, new File("/storage/emulated/0"), false);
+
+ assertThat(values.getAsString(MediaColumns.DATA)).isEqualTo(
+ "/storage/emulated/0/Android/abc.txt");
+ }
+
+ @Test
+ public void testComputeDataFromValuesForInvalidPath_throwsIllegalArgumentException() {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.RELATIVE_PATH, "\0");
+ values.put(MediaColumns.DISPLAY_NAME, "./../../abc.txt");
+
+ assertThrows(IllegalArgumentException.class,
+ () -> FileUtils.computeDataFromValues(values, new File("/storage/emulated/0"),
+ false));
+ }
}