The vulnerability has been patched in October 2014 by checking if the package name of the downloaded APK is the same as the package name of UniversalMDMClient application. Since it is not possible to have two applications with the same package name and signed by two different certificates, it becomes impossible to install an arbitrary application.

If the user choose "yes", the application will do a GET on the update server URL and the body of the answer is saved as an APK file. Last but not least it will be installed without prompting the user about the permission asked by the application or checking the certificate used to sign the APK. Hence, if an attacker can trick the user into accepting the update, he can install an arbitrary application with arbitrary permissions.

After having extracted the update server URL, the application will try to do a HEAD on the URL. It will check if the server returns the non standard header " x-amz-meta-apk-version ". If this happens, it will compare the current version of the UniversalMDMClient application to the version specified in the " x-amz-meta-apk-version " header. If the version in the header is more recent, it will show a popup to the user explaining that an update is available and asking if he wants to install it or no.

The UniversalMDMClient application is installed by default as part of Samsung KNOX. It registers a custom URI " smdm:// ". When an user clicks on a link to open an URL starting by " smdm:// ", the component LaunchActivity of UniversalMDMClient will be started and will parse the URL. Many information is extracted from the URL, and among them an update server URL.

Detailed writeup

Looking at the UniversalMDMClient's AndroidManifest.xml file, we can see that it defines a custom URI:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <manifest android:versionCode= "2" android:versionName= "1.1.14" package= "com.sec.enterprise.knox.cloudmdm.smdms" xmlns:android= "http://schemas.android.com/apk/res/android" > <uses-sdk android:minSdkVersion= "17" android:targetSdkVersion= "19" /> [...] <uses-permission android:name= "android.permission.INSTALL_PACKAGES" /> [...] <application android:allowBackup= "true" android:name= ".core.Core" > <activity android:configChanges= "keyboard|keyboardHidden|orientation" android:excludeFromRecents= "true" android:label= "@string/titlebar" android:name= ".ui.LaunchActivity" android:noHistory= "true" android:theme= "@android:style/Theme.DeviceDefault" > <intent-filter> <data android:scheme= "smdm" /> <action android:name= "android.intent.action.VIEW" /> <category android:name= "android.intent.category.DEFAULT" /> <category android:name= "android.intent.category.BROWSABLE" /> </intent-filter> </activity> [...] </application> </manifest>

The intent-filter registers (line 11-16) the custom URI "smdm://" and associates it to the com.sec.enterprise.knox.cloudmdm.smdms.ui.LaunchActivity component. When the user (or his browser ;) ) will try to open a "smdm://" URI, the onCreate() method of LaunchActivity will handle the situation. From here, we will dive into the code. Besides the application has been ""obfuscated"" by proguard, it is not really a problem to analyse it using the JEB decompiler and his ability to let the user rename methods and classes.

Below is the source code decompiled and renamed of the onCreate() method:

The first thing done by the onCreate() method is to check (via the function getPreETAG()) the presence of a file named PreETag.xml inside the directory /data/data/com.sec.enterprise.knox.cloudmdm.smdms/shared_prefs/. If the file exists, the application aborts its execution by calling the finish() method. By default, the file PreETag.xml does not exit.

The application will now try to get the Intent used to start the Activity and more precisely the data attached to it. The data must be of the form "smdm://hostname?var1=value1&var2=value2". The parsed variables names can be easily obtained from the source: seg_url, update_url, email, mdm_token, program and quickstart_url. The most important is update_url. After writing all these variables values inside a shared_preference file, onCreate() ends by calling Core.startSelfUpdateCheck():

The Core.startSelfUpdateCheck() checks if an update is currently in progress, if not it calls UMCSelfUpdateManager.startSelfUpdateCheck():

The function verifies that data connection is available, deletes pending update if present, constructs an URL based on the value of the umc_cdn string inside the shared_pref file "m.xml" and append to it the constant string "/latest". The value of umc_cdn is the value of our Intent data variable udpdate_url. So this is a value fully controlled by an attacker. It then calls UMCSelfUpdateManager.doUpdateCheck() with as first parameter the previously constructed URL:

Inside this function, a ContentTransferManager class instance is initialized and a HEAD HTTP request is performed on the attacker controlled URL. The different states which will be encountered during the life of the HTTP request are handled by the handleRequestResult class and methods onFailure(), onProgress(), onStart(), onSucess(), etc.

The most interesting method is of course onSucess(). It checks that different headers are present : ETag, Content-Length and x-amz-meta-apk-version. The value of the header x-amz-meta-apk-version is compared to the current UniversalMDMApplication APK package version. If the header x-amz-meta-apk-version contains a number bigger than the current APK version, then an update is needed.

At this point a popup appears on the screen of the user, explaining that an update for his application is available and asking if he wants to install it or no. If he chooses yes, we can continue our analysis, and the attack.

If the user chooses "yes", UMCSelfUpdateManager.onSuccess() is called, and before returning, it calls his parent onSucess() method which has the following code:

This onSuccess() will finaly call beginUpdateProcess() which will start an update thread:

The update thread will call installApk() which in turn will call _installApplication() whose role is to disable the package verifier (to prevent Google from scanning the APK at the installation), install the APK and reenable the packager verifier:

And... that's all. At no time the download APK authenticity is checked nor the asked permissions are showed to the user. Hence, this vulnerability allows an attacker to install an arbitrary application.

Once the update have been installed, it is not possible to exploit the vulnerability anymore because when a successful update has been installed, the value of the ETag header is written in /data/data/com.sec.enterprise.knox.cloudmdm.smdms/shared_prefs/PreETag.xml, and the existence of the file is the first check done inside the onCreate() method of LaunchActivity. If the file already exists, the method calls finish() and the execution aborts.