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.

Important

After this blogpost was published, it got spread and I managed to find the original reporter of the bug via a Telegram discussion about it! I had the opportunity to speak to him about it. You can find him on X at @vulnano and LinkedIn at Dzmitry Lukyanenko

Please see my updated conclusions at Update (05, Dec, 2024)

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.

Update (05, Dec, 2024)

This bug can be exploited to launch privileged activities, however, as I suspected it requires chaining another bug of a race condition which did not affect my OnePlus device because my Android Security Path Level was more than the one of the bug reporter.

After speaking with the original bug reporter @vulnano, he taught me a pretty awesome trick he used to launch un-exported activities of other apps.

The setup goes as follows:

  1. You create a Dummy activity with the same name of intent of the target app, for example, DummyActivity will have an intent with a name of com.NotYourApp.gmail.SEND_EMAIL.
  2. You create a Content Provider HackyContentProvider that will count the number of times getType() was called.
    1. Then, after a few times(around 3~) getType() was called, you call ActivityManager and disable the DummyActivity.
  3. In the account_preferences.xml, you don’t specify a target APK or target class. Instead:
    1. you specify an intent that your malicious app
    2. you specify a <data> attribute of the content provider created at step 2, this will trigger getType() couple of times when the activity will be launched by the settings app.

The attack flow goes as follows:

  1. isSafeIntent() returns true.
  2. The mFragment.getActivity().startActivityAsUser() function is called
    1. It looks in the account_preferences.xml file no activity is specified, only an implicit intent
    2. Android will look up what apps can handle that implicit intent, it gets two results: the ‘email’ app, and your app(specifically the DummyActivity, that also has the same intent)
    3. At that point of time(before the activity was launched and after the isSafeIntent() returned true: your HackyContentProvider will disable the DummyActivity, turning it into an activity that can’t be launched.
    4. As a result, Android will launch the other option from the two it found earlier(the first one was your app, the second was the email app) with the permissions of the settings.apk app!
  3. profit