昨天出去玩了,没带电脑,所以今天补上昨天的。2023.09.17补2023.09.16

CVE-2022-20395 A-221855295 EoP High 11, 12, 12L, 13

patch

分析

在MediaProvider的deleteIfAllowed函数里存在路径穿越漏洞,会导致任意文件删除。patch是改为使用getCanonicalFilegetCanonicalPath,对路径进行正规化之后再使用。

  • 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));
+    }
 }