The Vulnerability

The following analysis has been done on a Samsung Galaxy S6. The vulnerable code is located in the application Hs20Settings.apk . It registers a BroadcastReceiver named WifiHs20BroadcastReceiver which is executed at boot, and also on some WiFi events ( android.net.wifi.STATE_CHANGE ).

Let us note that the vulnerable code can be located elsewhere on some Samsung devices. For example, on the Samsung Galaxy S5, the vulnerable code is in the application SecSettings.apk instead.

When the BroadcastReceiver is triggered by one of the previous event, the following code is executed:

public void onReceive ( Context context , Intent intent ) { [...] String action = intent . getAction (); [...] if ( "android.intent.action.BOOT_COMPLETED" . equals ( action )) { serviceIntent = new Intent ( context , WifiHs20UtilityService . class ); args = new Bundle (); args . putInt ( "com.android.settings.wifi.hs20.utility_action_type" , 5003 ); serviceIntent . putExtras ( args ); context . startServiceAsUser ( serviceIntent , UserHandle . CURRENT ); } [...] }

For each event received, an Intent is created to spawn a service named WifiHs20UtilityService . Looking at the "constructor" of the service, specifically the onCreate() method, we can see the creation of a new object WifiHs20CredFileObserver :

public void onCreate () { super . onCreate (); Log . i ( "Hs20UtilService" , "onCreate" ); [...] WifiHs20UtilityService . credFileObserver = new WifiHs20CredFileObserver ( this , Environment . getExternalStorageDirectory (). toString () + "/Download/" ); WifiHs20UtilityService . credFileObserver . startWatching (); [...] }

The WifiHs20CredFileObserver is defined as a Java subclass of FileObserver :

class WifiHs20CredFileObserver extends FileObserver {

The FileObserver object is defined as below in the Android documentation :

Monitors files (using inotify) to fire an event after files are accessed or changed by any process on the device (including this one). FileObserver is an abstract class; subclasses must implement the event handler onEvent(int, String). Each FileObserver instance monitors a single file or directory. If a directory is monitored, events will be triggered for all files and subdirectories inside the monitored directory. An event mask is used to specify which changes or actions to report. Event type constants are used to describe the possible changes in the event mask as well as what actually happened in event callbacks.

The public constructor must specify a path and a mask for the monitored events:

FileObserver ( String path , int mask )

The constructor of WifiHs20CredFileObserver is the following:

public WifiHs20CredFileObserver ( WifiHs20UtilityService arg2 , String path ) { WifiHs20UtilityService . this = arg2 ; super ( path , 0xFFF ); this . pathToWatch = path ; }

In the above code snippet, the FileObserver watches for all valid types of events on the /sdcard/Download/ directory. Indeed, the mask 0xFFF is for FileObserver.ALL_EVENTS . To understand the action taken when an event is received, we have to look at the overriden method onEvent() in WifiHs20CredFileObserver :

public void onEvent ( int event , String fileName ) { WifiInfo wifiInfo ; Iterator i$ ; String credInfo ; if ( event == 8 && ( fileName . startsWith ( "cred" )) && (( fileName . endsWith ( ".conf" )) || ( fileName . endsWith ( ".zip" )))) { Log . i ( "Hs20UtilService" , "File CLOSE_WRITE [" + this . pathToWatch + fileName + "]" + event ); if ( fileName . endsWith ( ".conf" )) { try { credInfo = this . readSdcard ( this . pathToWatch + fileName ); if ( credInfo == null ) { return ; } new File ( this . pathToWatch + fileName ). delete (); i$ = WifiHs20UtilityService . this . expiryTimerList . iterator (); while ( i$ . hasNext ()) { WifiHs20Timer . access$500 ( i$ . next ()). cancel (); } WifiHs20UtilityService . this . expiryTimerList . clear (); WifiHs20UtilityService . this . mWifiManager . modifyPasspointCred ( credInfo ); wifiInfo = WifiHs20UtilityService . this . mWifiManager . getConnectionInfo (); if (! wifiInfo . isCaptivePortal ()) { return ; } if ( wifiInfo . getNetworkId () == - 1 ) { return ; } WifiHs20UtilityService . this . mWifiManager . forget ( WifiHs20UtilityService . this . mWifiManager . getConnectionInfo (). getNetworkId (), null ); } catch ( Exception e ) { e . printStackTrace (); } return ; } if ( fileName . endsWith ( ".zip" )) { String zipFile = this . pathToWatch + "/cred.zip" ; String unzipLocation = "/data/bundle/" ; if (! this . installPathExists ()) { return ; } this . unzip ( zipFile , unzipLocation ); new File ( zipFile ). delete (); credInfo = this . loadCred ( unzipLocation ); if ( credInfo == null ) { return ; } i$ = WifiHs20UtilityService . this . expiryTimerList . iterator (); while ( i$ . hasNext ()) { WifiHs20Timer . access$500 ( i$ . next ()). cancel (); } WifiHs20UtilityService . this . expiryTimerList . clear (); Message msg = new Message (); Bundle b = new Bundle (); b . putString ( "cred" , credInfo ); msg . obj = b ; msg . what = 42 ; WifiHs20UtilityService . this . mWifiManager . callSECApi ( msg ); wifiInfo = WifiHs20UtilityService . this . mWifiManager . getConnectionInfo (); if (! wifiInfo . isCaptivePortal ()) { return ; } if ( wifiInfo . getNetworkId () == - 1 ) { return ; } WifiHs20UtilityService . this . mWifiManager . forget ( WifiHs20UtilityService . this . mWifiManager . getConnectionInfo (). getNetworkId (), null ); } } }

When an event of type 8 ( FileObserver.CLOSE_WRITE ) is received, some checks are done on the filename and actions may be taken. If the written filename begins with cred and ends with .conf or .zip , then some processing is performed. In all other cases, the FileObserver simply ignores it.

When an interesting file is written in the monitored folder, two scenarios can happen:

It's a .conf file: the service reads the file calling readSdcard() , then the configuration is passed to WifiManager.modifyPasspointCred() and, finally, the .conf file is deleted after the call to readSdcard() .

file: the service reads the file calling , then the configuration is passed to and, finally, the file is deleted after the call to . It's a .zip file: the service extracts it to /data/bundle/ and it calls loadCred() to parse the content of a cred.conf file extracted. Then, it calls WifiManager.callSECApi() with the result from loadCred() as an argument inside a Bundle object. The .zip file is deleted after the unzip operation.

The first case is not really interesting to us, but the second one is. The unzip operation is done using the standard ZipInputStream class, and it's a well known issue that if no validation is done on the filenames inside the archive, a directory traversal can be performed. The vulnerability is similar to the one reported by @fuzion24 in the samsung keyboard update mechanism .

The following is a cleaned version of the unzip() function. For readability, try/catch statements not necessary to the comprehension of the function have been removed:

private void unzip ( String _zipFile , String _location ) { FileInputStream fin = new FileInputStream ( _zipFile ); ZipInputStream zin = new ZipInputStream ((( InputStream ) fin )); ZipEntry zentry ; /* check if we need to create some directories ... */ while ( true ) { label_5 : zentry = zin . getNextEntry (); if ( zentry == null ) { // exit } Log . v ( "Hs20UtilService" , "Unzipping********** " + zentry . getName ()); if (! zentry . isDirectory ()) { break ; } /* if the directory does'nt exist, the _dirChecker will create it */ this . _dirChecker ( _location , zentry . getName ()); } FileOutputStream fout = new FileOutputStream ( _location + zentry . getName ()); int c ; for ( c = zin . read (); c != - 1 ; c = zin . read ()) { if ( fout != null ) { fout . write ( c ); } } if ( zin != null ) { zin . closeEntry (); } if ( fout == null ) { goto label_45 ; } fout . close (); label_45: MimeTypeMap type = MimeTypeMap . getSingleton (); String fileName = new String ( zentry . getName ()); int i = fileName . lastIndexOf ( 46 ); if ( i <= 0 ) { goto label_5 ; } String v2 = fileName . substring ( i + 1 ); Log . v ( "Hs20UtilService" , "Ext" + v2 ); Log . v ( "Hs20UtilService" , "Mime Type" + type . getMimeTypeFromExtension ( v2 )); goto label_5 ; } }

One can notice that files in the archive are not verified for potential directory traversal issues. Hence, if we have a file cred.zip or cred[something].zip written in the /sdcard/Download/ directory, WifiHs20CredFileObserver automatically (i.e, without any user interaction) extracts the content of the archive in the /data/bundle/ directory and deletes the zip file afterwards. As no verification on filenames is performed in the decompression routine, any file within the archive beginning with ../ is extracted outside the /data/bundle/ directory and existing files are overwritten. Keep in mind that the unzip operation is done as system user.

Now, let us think about how to transform that into code execution.