home/blog/disclosed


Reproducing Disclosed Bugs

2020-03-04

[IRCCloud Android] Theft of arbitrary files leading to token leakage

https://hackerone.com/reports/288955 This app is open-source so we can directly look at the source instead of downloaded an APK file. The report was filed on 10th November 2017, so download a release before that. Once we unzip the source, we can work on it in Android Studio like any other project. There are a few issues with making the android project work. First you need to remove version controlling from the project by modifying build gradle wherever necesssary (by reading the build errors). Or you can clone the repository from the particular commit you are looking for and the version control will take care of itself. The other issue is android sdk version. Unfortunately even sorting these two issues, I wasn't able to build the app. But even without building the app, if you have the source, it becomes trivial to debug the app. Anyway, I just saw at the end of the report, bagipro has attached the affected apk, so we can use the source from github while use the vulnerable apk from the report. In AndroidManifest.xml
<activity
    android:name=".activity.ShareChooserActivity"
    android:excludeFromRecents="true"
    android:theme="@style/dawnDialog">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />

        <data android:mimeType="application/*" />
        <data android:mimeType="audio/*" />
        <data android:mimeType="image/*" />
        <data android:mimeType="text/*" />
        <data android:mimeType="video/*" />
    </intent-filter>

    <meta-data
        android:name="android.service.chooser.chooser_target_service"
        android:value=".ConversationChooserTargetService" />
</activity>
In ShareChooseActivity within onResume function, we see:
if(getIntent() != null && getIntent().hasExtra(Intent.EXTRA_STREAM)) {
    mUri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
    if(mUri != null)
        mUri = MainActivity.makeTempCopy(mUri, this);
// ... 
For makeTempCopy there were two overloaded methods, at the end of the makeTempCopy(Uri, Context) we have a call to makeTempCopy(fileUri, context, original_filename); I have commented the codes below to follow along the report. Please pay attention to the comments.
public static Uri makeTempCopy(Uri fileUri, Context context) {
    if(fileUri == null) // if there is no file uri provided
        return null;    // return null as the temp uri

    String original_filename; // path to the original file

    if (Build.VERSION.SDK_INT < 16) { // if the sdk version is less than 16
        original_filename = fileUri.getLastPathSegment(); // get the last segment in a path. ex: c in /a/b/c
    } else { // for higher api versions we perhaps require a more complicated method of extracting last segment
        Cursor cursor = null; // cursor object to go over results of some query
        try {
            cursor = context.getContentResolver().query(fileUri, null, null, null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
                original_filename = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            } else {
                original_filename = fileUri.getLastPathSegment();
            }
        } catch (Exception e) {
            e.printStackTrace();
            original_filename = String.valueOf(System.currentTimeMillis());
        }

        if(cursor != null)
            cursor.close();
    }

    if(original_filename == null || original_filename.length() == 0)
        original_filename = "file";

	// after all that effort to extract the original file name we pass it on to the next method
    return makeTempCopy(fileUri, context, original_filename);
}
public static Uri makeTempCopy(Uri fileUri, Context context, String original_filename) {

	// checking if there is indeed a uri passed

	if(fileUri == null)
        return null;

	// if the uri string already contains absolute path of the application's cache directory, simply return it

    if(fileUri.toString().contains(context.getCacheDir().getAbsolutePath()))
        return fileUri;

	// setting type using getType, if not found, then using extension

    String type = context.getContentResolver().getType(fileUri);
    if (type == null) {
        String lower = original_filename.toLowerCase();
        if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
            type = "image/jpeg";
        else if (lower.endsWith(".png"))
            type = "image/png";
        else if (lower.endsWith(".gif"))
            type = "image/gif";
        else if (lower.endsWith(".webp"))
            type = "image/webp";
        else if (lower.endsWith(".bmp"))
            type = "image/bmp";
        else if (lower.endsWith(".mp4"))
            type = "video/mp4";
        else if (lower.endsWith(".3gpp"))
            type = "video/3gpp";
        else
            type = "application/octet-stream";
    }

	// if the original file doesn't contain an extension, then add one using the type as inferred above

    if (!original_filename.contains("."))
        original_filename += "." + type.substring(type.indexOf("/") + 1);

    try {
        Uri out = Uri.fromFile(new File(context.getCacheDir(), original_filename)); // output url determined by getCacheDir()
        Log.d("IRCCloud", "Copying file to " + out); // logging whatever's happening, can be seen using logcat
        InputStream is = IRCCloudApplication.getInstance().getApplicationContext().getContentResolver().openInputStream(fileUri);
        OutputStream os = IRCCloudApplication.getInstance().getApplicationContext().getContentResolver().openOutputStream(out);
        byte[] buffer = new byte[8192];
        int len; // copying original file to temp file
        while ((len = is.read(buffer)) != -1) {
            os.write(buffer, 0, len);
        }
        is.close();
        os.close();
        return out;
    } catch (Exception e) {
        e.printStackTrace();
        return fileUri;
    }
}
So, then where is the issue? The problem is that the getLastPathSegment() gets the last path of the provided file, but the path can be something like ..%2F..%2F..%2F..%2Fsdcard%2Fprefs.xml relative path from "/data/data/com.irccloud.android/cache/.

SQL Injection found in NextCloud Android App Content Provider

https://hackerone.com/reports/291764 We see in the disclosed report they have provided a pull request which resolves the SQL injection issue: https://github.com/nextcloud/android/pull/1820 The app was fixed in release 2018-01-06, so we should look at the version just before it. I was able to find this APK file released before (2017-05-23) the fix: https://download.nextcloud.com/android/nextcloud-100403.apk
dz> run scanner.provider.injection -a com.nextcloud.client

-snip-

Injection in Projection:
  No vulnerabilities found.

Injection in Selection:
  No vulnerabilities found.

-snip-
Unfortunately I wasn't able to reproduce the injection using the same command as that in the report. So I decided to decompile and see whether the vulnerable activity was still exported. And it wasn't, therefore they must have not yet implmented the patch. As we can see in the pull request (https://github.com/nextcloud/android/pull/1820/commits/0dc26336137463929f3b8d1a85765cf11edcc1e4) that exported attribute was set to false, but in the decompiled code below there is no such attribute.
<activity android:theme="@style/Theme.ownCloud.Dialog.NoTitle" android:label="@string/share_dialog_title" android:name="com.owncloud.android.ui.activity.ShareActivity" android:launchMode="singleTop" android:windowSoftInputMode="adjustResize">
    <intent-filter>
        <action android:name="android.intent.action.SEARCH"/>
    </intent-filter>
    <meta-data android:name="android.app.searchable" android:resource="@xml/users_and_groups_searchable"/>
</activity>
We can also see that the lines in src/main/java/com/owncloud/android/providers/FileContentProvider.java are the same before the pull:
Cursor c = sqlQuery.query(db, projection, selection, selectionArgs, null, null, order);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
So the bug is still there in the APK I downloaded, why was it not being detected by drozer? After some time, I decided to go with an emulator and supplied the same command and it worked!
dz>  run scanner.provider.injection -a com.nextcloud.client

-snip-

Injection in Projection:
  content://org.nextcloud/
  content://org.nextcloud

Injection in Selection:
  content://org.nextcloud/
  content://org.nextcloud

-snip-

dz> run app.provider.query content://org.nextcloud/ --projection "'"
unrecognized token: "' FROM filelist ORDER BY filename collate nocase asc" (code 1 SQLITE_ERROR): , while compiling: SELECT ' FROM filelist ORDER BY filename collate nocase asc
And so on, the other SQL injection commands in the report worked as well, which could help leak the database. An important question to ask: why did drozer not work in the physical device but worked in the emulator? For both the physical and virtual device API_LEVEL is 28.

[Zomato Android/iOS] Theft of user session

https://hackerone.com/reports/328486 The bug was submitted on 22nd March 2018, so I downloaded this APK from 30th December 2018. Before understanding how the bug works, I first check whether the bug is reproducible. The following code does indeed spawn a website within Zomato's webview.
adb shell am start -n com.application.zomato/.activities.DeepLinkRouter -a android.intent.action.VIEW -d "zomato://treatswebview/?url=http://google.com&navigation_bar_title=wow"
But the above PoC only works through the help of another third party app in the same phone. The report also disclosed a way to do the same thing using a link. I hosted the below code using python -m SimpleHTTPServer 8000 on my computer and then put [my computer's ip]:8000 on my emulator and it worked.
<!DOCTYPE html>
<html>
<head><title>Zaheck page</title></head>
<body style="text-align: center;">
    <h1><a href="zomato://treatswebview/?url=http://google.com&navigation_bar_title=wow">Begin zaheck!</a></h1>
</body>
</html>
Now that we know the PoC works, let's see what is actually happening. After decompiling and checking the manifest, we see that this activity has an intent-filter for BROWSABLE therefore it can accessed by any browser.
<activity android:theme="@style/ZomatoTranslucentTheme" android:label="@string/app_name" android:name="com.application.zomato.activities.DeepLinkRouter" android:screenOrientation="portrait">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="zomato"/>
    </intent-filter>
</activity>
We can see that the class that handles this intent is DeepLinkRouter.java unfortunately some of the code in here is still not decompiled such as the method c below. I tried using dex2jar and jd-gui after attempting jadx-gui but neither worked.
private void c(java.lang.String r9) {
        /*
            r8 = this;
            r6 = 2
            r2 = 1
            r1 = 0
            android.net.Uri r4 = android.net.Uri.parse(r9)     // Catch:{ Exception -> 0x00d7 }
            java.lang.String r0 = "zomato"
            java.lang.String r3 = r4.getScheme()
In the report, we can see the decompiled version of the code (I want to know what tool, or manual testing? was used but anyway) it seems that a URL is extracted from the intent data and "zomato".equals(parse.getScheme()) and "treatswebview".equals(host) we ascertain that the URL should look something like: zomato://treatswebview?url=. We also have uri.getQueryParameter("url") and uri.getQueryParameter("navigation_bar_title") parameters which set the url and navigation title bar respectively. Then finally all this data goes into another class TreatsWebViewActionBarActivity.java where a WebView is created and displayed. But what is the impact?: you can steal any session data that goes with the request. Would I be able to do it?: I would probably give up at the non-decompiled method. :(

Periscope android app deeplink leads to CSRF in follow action

https://hackerone.com/reports/583987 Looking at AndroidManifest.xml we see that for AppRouterActivity there are several intent-filters. Some of them have HTTP or HTTPS as the scheme. But there are others with PSCP or PSCPD. The one used in the report is as follows:
<data android:scheme="pscpd" android:host="user" android:pathPrefix="/"/>
I create a SimpleHTTPServer and check the difference between a normal link and a deeplink, and just like in the report, the deeplink works without confirmation.
<!DOCTYPE lt;html>
<html>
<a href="pscp://user/LCFC/follow">deep-link</a>
<a href="https://www.pscp.tv/LCFC/follow">other</a>
</html>
I wonder though, why we have to create a PoC page and why does the browser itself not infer a deep-link through the URL bar?

Twitter lite(Android): Vulnerable to local file steal, Javascript injection, Open redirect

https://hackerone.com/reports/499348 After downloading required APK version, I go ahead and decompile it. We can see in the manifest that TwitterLiteActivity has an intent filter for BROWSABLE and this is what the report exploits. To see how the intent is captured we need to see the corresponding class in java.
<activity android:name="com.twitter.android.lite.TwitterLiteActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:hardwareAccelerated="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="http"/>
        <data android:scheme="https"/>
        <data android:host="mobile.twitter.com"/>
        <data android:pathPattern="/.*"/>
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
</activity>
This snippet in TwitterLiteActivity allows for most exploitation:
this.a.getSettings().setJavaScriptEnabled(true);
this.a.addJavascriptInterface(new ApkRwebInterface(this), "apkInterface");
this.a.getSettings().setAllowFileAccess(true);
this.a.getSettings().setDomStorageEnabled(true);
this.a.getSettings().setUserAgentString(a2);
Some more code on how the WebView is set and how it parses data:
private WebView a;

if ("android.intent.action.SEND".equals(getIntent().getAction()) && "text/plain".equals(getIntent().getType())) {
    this.a.loadUrl(d.a(getIntent().getStringExtra("android.intent.extra.TEXT")).toString());
} else if (data != null) {
    this.a.loadUrl(data.toString());
} else {
    this.a.loadUrl("https://mobile.twitter.com");
}
Once you know how and where a particular external event is captured, it might be a good idea to see how it works dynamically with frida. As of now, I am not familiar with frida, so I am just going to go ahead and assume that this is the method that handles our intent. Looking at the above code we see that if no data is passed it defaults to mobile.twitter.com otherwise it opens the passed URI.
# opens default webpage
$ adb shell am start -n com.twitter.android.lite/com.twitter.android.lite.TwitterLiteActivity
Starting: Intent { cmp=com.twitter.android.lite/.TwitterLiteActivity }

# tried to open file as in report,  but get ERR_ACCESS_DENIED
$ adb shell am start -n com.twitter.android.lite/com.twitter.android.lite.TwitterLiteActivity -d "file:///sdcard/Download/secret.txt"
Starting: Intent { dat=file:///sdcard/Download/secret.txt cmp=com.twitter.android.lite/.TwitterLiteActivity }

# XSS
$ adb shell am start -n com.twitter.android.lite/com.twitter.android.lite.TwitterLiteActivity -d "javascript://example.com%0Aalert\(1\);"
So essentially, it opens any URL without validation.

[Grab Android/iOS] Insecure deeplink leads to sensitive information disclosure

https://hackerone.com/reports/401793 In this post, I am going to go over a report regarding missing validation in deeplinks in grab. So first we are going to get the affected APK file. We can see that the report is filed on March 16, 2019 5:11am +0530. This means that we can go to APKMirror and retrieve a file that precedes the given date. We can also check how the patch if we go through the later revisions. I downloaded the file and tried to make it work first an my emulator then on my physical device. I was not able to login, perhaps due to not having a phone number within a country where Grab is available. Anyway, moving on to the next one.

[TinyCards] RCE in TinyCards for Android

https://hackerone.com/reports/281605 https://hackerone.com/reports/293444 Both the reports are not accessible anymore, so I looked around the internet for other sources of disclosure, and found this: https://seclists.org/fulldisclosure/2018/Jan/16 But, unfortunately I couldn't find the affected APK version the earliest version found on APKMirror was v1.0 which was already fixed.