Note: This is part of my @vr_progress journal. Also, subscribe to my new @SideQuest_256 channel and I might post videos about the Android journey too :D

This is a story about how I wasted my weekend over a bug that was categorized as a High/EoP but then couldn’t find a clever way to elevate privileges with it.

It all started when I decided to revisit an old testing device - my trusty OnePlus phone that had been sitting in a drawer, untouched and unpatched for years. Since I hadn’t updated it in ages, I thought it would be fun to try exploiting a 1-day vulnerability that affected the device.

Scrolling through the Android advisories, CVE-2020-0238 caught my attention, and I thought, “Why not?” After all, the bug was labeled as High severity, with the potential for Elevation of Privilege (EoP). Also, it’s a logic bug so I might even get a quick win(unlike memory corruptions that usually take more time and special engineering to exploit)

What followed was a weekend full of debugging, writing PoC code, and realizing that, while the bug seemed interesting on the surface, its real-world exploitability has more pre-requisites than I thought.

  • What I expected: Direct EoP from a untrusted_app to a privileged settings.apk .
  • What I got: Potential/Indirect EoP from attacker apk to a victim apk.

Here’s what I learned about CVE-2020-0238, its fix, and its impact.

The Fix

Below is the fix:

index d32b630..c639d1d 100644
--- a/src/com/android/settings/accounts/AccountTypePreferenceLoader.java
+++ b/src/com/android/settings/accounts/AccountTypePreferenceLoader.java
 
@@ -197,14 +197,7 @@
         ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo;
         ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo;
         try {
-            if (resolvedActivityInfo.exported) {
-                if (resolvedActivityInfo.permission == null) {
-                    return true; // exported activity without permission.
-                } else if (pm.checkPermission(resolvedActivityInfo.permission,
-                    authDesc.packageName) == PackageManager.PERMISSION_GRANTED) {
-                    return true;
-                }
-            }
+            // Allows to launch only authenticator owned activities.
             ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0);
             return resolvedAppInfo.uid == authenticatorAppInf.uid;
         } catch (NameNotFoundException e) {

Analysis

The issue revolves around the AccountTypePreferenceLoader class in Android’s Settings app, specifically in how it resolved and validated activities associated with an account type. The problem is triggered from the AccountDetailDashboardFragment class(in com.android.settings.accounts), where user accounts are managed.

  1. AbstractAccountAuthenticator: This is a base class that app developers use to create custom authenticators for managing accounts on Android. It defines methods like addAccount() or getAuthToken().
  2. AccountTypePreferenceLoader: This class is responsible for loading UI elements (preferences) for account types in the Settings app. It would erroneously allow launching activities from other apps if certain conditions were met.
  3. The Bug: The check for resolvedActivityInfo.exported allowed activities not owned by the authenticator to be launched if they were exported and did not require specific permissions.

The fix ensures that only activities belonging to the same uid as the authenticator’s app can be launched, reducing the risk of privilege escalation.

Re-produce

To exploit this bug, a malicious app needs to manipulate the account_preferences.xml file and register a custom service that interacts with the vulnerable component.

  1. Create a Service: Define a malicious service in the app’s manifest to mimic an authenticator:
<service
    android:name=".MaliciousAuthenticatorService"
    android:permission="android.permission.BIND_ACCOUNT_AUTHENTICATOR">
    <intent-filter>
        <action android:name="android.accounts.AccountAuthenticator" />
    </intent-filter>
</service>
  1. Implement addAccount: Implement a basic addAccount() method in your malicious authenticator service to register an account.
  2. Craft account_preferences.xml: Create a custom account_preferences.xml file with entries designed to trigger the vulnerable code path.
<PreferenceScreen android:key="launch_bug"  
	android:title="Trigger bug :D"  
	android:summary="Triggering CVE-2020-0238">  
	<intent android:action="android.intent.action.MAIN"  
		android:targetPackage="com.pwnable.verysecure"  
		android:targetClass="com.pwnable.verysecure.VerySecure" />  
</PreferenceScreen>

When the Settings app attempts to load preferences for this malicious account type, the bug allows launching unauthorized activities.

When clicking on the account, the AccountDetailDashboardFragment is loaded, together with our xml:

Then, a click on the “Trigger bug :D” button, triggers updatePreferenceIntents(), which is calling the isSafeIntent() function(the one that the commit patched)

public void updatePreferenceIntents(PreferenceGroup prefs, final String acccountType,
		Account account) {
	final PackageManager pm = mFragment.getActivity().getPackageManager();
	for (int i = 0; i < prefs.getPreferenceCount(); ) {
		Preference pref = prefs.getPreference(i);
		if (pref instanceof PreferenceGroup) {
			updatePreferenceIntents((PreferenceGroup) pref, acccountType, account);
		}
		Intent intent = pref.getIntent();
		if (intent != null) {
			if (TextUtils.equals(intent.getAction(),
					android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) {
				// The OnPreferenceClickListener overrides the click event completely. No intent
				// will get fired.
				pref.setOnPreferenceClickListener(new FragmentStarter(
					LocationSettings.class.getName(), R.string.location_settings_title));
			} else {
				ResolveInfo ri = pm.resolveActivityAsUser(intent,
					PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier());
				if (ri == null) {
					prefs.removePreference(pref);
					continue;
				}
				intent.putExtra(ACCOUNT_KEY, account);
				intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
				pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
					@Override
					public boolean onPreferenceClick(Preference preference) {
						Intent prefIntent = preference.getIntent();
						if (isSafeIntent(pm, prefIntent, acccountType)) {
							mFragment.getActivity().startActivityAsUser(
								prefIntent, mUserHandle);
						} else {
							Log.e(TAG,
								"Refusing to launch authenticator intent because"
									+ "it exploits Settings permissions: "
									+ prefIntent);
						}
						return true;
					}
				});
			}
		}
		i++;
	}
}

Impact

I’m not really sure how exploitable this is, because the attacker APK needs to have the same permission as the victim APK(at least on my testing device).

Also, if the activity is already exported, why would the attacker app need to use the Settings app to trigger the exported activity? It’s already exported.

The only scenario I could think of is one where the victim app has an exported activity but it verifies which app has triggered the activity (using stuff like getCallingUid() and getCallingPackage()), allowing only the Settings app to trigger it.

The fix mitigates these risks by enforcing that only activities owned by the authenticator can be launched. This change effectively ties activity execution to the authenticator’s uid, preventing unauthorized apps from exploiting this pathway.

Conclusion

In the end, CVE-2020-0238 demonstrates the subtlety of Android security issues - how seemingly innocuous permissions or checks can open doors for unintended behavior. This particular bug was a little bit disappointing, but, whatever. I hope I missed something and find out that it’s more exploitable than I initially thought.

Either way, I learned some new things, which is nice :^)

Thanks for tuning in, and see you in the next attempt.