A Deep Dive into the CoSoSys EndPoint Protector Exploit: Remote Code Execution
Intro
During an Advanced Persistent Threat (APT) simulation project for one of our clients, we (Security Assessment Team at Theori) uncovered four critical Remote Code Execution (RCE) vulnerabilities in the Endpoint Protector (EPP) solution by CoSoSys. These vulnerabilities allowed us to fully compromise both the EPP server and its clients, potentially leading to the theft of highly sensitive information. In our mission to contribute to a safer world, we promptly reported these vulnerabilities and provided the corresponding exploit codes to Netwrix, the vendor responsible for the solution, upon the project’s completion. These vulnerabilities have since been officially recognized and assigned the following CVE numbers.
CVE-2024–36072 (CVSSv4 10): This vulnerability allows an unauthenticated attacker to exploit a flaw in the logging component, enabling the execution of system commands with root privileges.
CVE-2024–36073 (CVSSv4 8.5): With administrative access to the server, this vulnerability allows an attacker to overwrite sensitive configurations and execute system commands on client endpoints.
CVE-2024–36074 (CVSSv4 7.3): This vulnerability enables an attacker with server access to execute malicious files.
CVE-2024–36075 (CVSSv4 7.2): Through this vulnerability, an unauthenticated attacker can manipulate client configurations, potentially bypassing security policies and achieving remote code execution in certain scenarios.
All of the vulnerabilities above have been patched as of now. In this post, we will talk about the background and analysis process that led to the discovery of these vulnerabilities in the EPP solution, as well as how we leveraged them to take control of the server and clients. We would also like to extend our thanks to Netwrix for their swift response in patching these vulnerabilities and for allowing us to disclose them publicly.
Background
We conducted a red-team project aimed at both strengthening our client’s security posture — internally and externally — and enhancing their employees’ security awareness. The primary objective of the engagement was to acquire as much sensitive information and internal assets as possible, with asset identification serving as the critical first step in our internal penetration strategy.
During the asset identification process, we discovered that the client was using CoSoSys’s EPP solution. Enterprise-wide solutions like EPP are particularly valuable targets for lateral movement attacks, as they provide centralized control over employee devices. Recognizing the critical role that lateral movement plays in the theft of corporate assets, we planned attacks targeting the EPP.
Initially, we searched for any previously discovered vulnerabilities that could be exploited. However, the most recent known vulnerability in CoSoSys’s EPP dated back to 2019, making it unlikely to be useful. Consequently, we proceeded with an in-depth vulnerability analysis of both the EPP server and client to uncover new attack vectors.
The EPP server’s source code, written in PHP and protected by Zend Guard, required us to use publicly available Dezend tools to retrieve the original code for analysis. Additionally, the macOS/Windows client was developed in C++, and since the source code could not be obtained directly, reverse engineering using static/dynamic analysis was required.
Endpoint Protector by CoSoSys
Endpoint Protector by CoSoSys offers Data Loss Prevention (DLP) capability designed to safeguard an organization’s sensitive media and track potential leakage paths. EPP includes an on-premise management server that enables administrators to set and enforce policies for monitoring connected client devices. Through these policies, the administrator can define which assets (e.g., documents, images, source code) should be monitored for movement within the company. Additionally, EPP supports blocking data leakage from various endpoints (e.g., shared folders, USB, email, messenger) and tracking data transfers.
Now, let’s explore the vulnerabilities of the EPP solution that we found and exploited during the APT attempt.
CVE-2024–36072: Insufficient input validation in file upload
The first vulnerability we will examine is CVE-2024–36072. This vulnerability enables an unauthenticated attacker to upload files to arbitrary paths, leading to an RCE vulnerability. Through this, the attacker can ultimately gain root shell access on the server.
To analyze this vulnerability, we began by examining the architecture of the EPP solution and identified that the core logic of the EPP solution is designed for authenticated users. The next step was to dissect the login mechanism, paying special attention to how it validates user credentials. The goal was to identify any vulnerabilities that could allow authentication bypass or enable attacks without authentication.
During our investigation, we discovered that functions requiring authentication are validated using the isSoapAuth
function, as shown below.
if ( $this->isSoapAuth( ) )
The function retrieves the SSL_CLIENT_S_DN_CN
field from the server variables, which contains the certificate information of the client attempting SSL communication. It then verifies whether the certificate is registered in the server’s database and whether a valid license exists, thereby identifying authenticated users. Below is the code for the function.
public function isSoapAuth( )
{
$this->machine_id = $_SERVER['SSL_CLIENT_S_DN_CN'];
$machine_in_db = $this->machineInDB( );
$machine_valid_license = $this->machineHasValidLicense( );
if ( $machine_in_db && $machine_valid_license )
{
return TRUE;
}
...
}
In the process of investigating how communication proceeds without authentication to potentially bypass the authentication mechanism, we discovered several handlers utilizing the SOAP protocol. Among these, we found the Register
handler, which is responsible for issuing certificates based on parameters provided by the user. Below is the code for the Register
handler.
public function Register( $ip, $mac, $name, $workgroup, $domain = "", $dcode = "" )
{
$goodCode = $this->checkDepartment( $dcode );
if ( $this->parameters['depusage'] == 2 && !$goodCode )
{
$dcode = "defdep";
}
if ( $this->parameters['depusage'] == 1 && !$goodCode )
{
$certificateReturn = array( "registrationCode" => -100, "certificate" => "", "password" => "", "serial" => "" );
return $certificateReturn;
}
$nameArray = explode( "/", $name );
$name = $nameArray[0];
$sid = $nameArray[1];
$startTime = microtime( TRUE );
$this->logThis( "--->Machine ".$ip.", {$mac}, {$name}, {$workgroup}, {$domain}, {$sid} is trying to register", 4 );
$clientMachineId = $this->checkForMachine( $ip, $mac, $name, $domain, $workgroup, $sid, $dcode );
$this->logThis( "--->Machine ".$clientMachineId." is registering", 4 );
$registerCode = $this->checkForLicense( $clientMachineId )[3];
$certpass = $this->checkForLicense( $clientMachineId )[2];
$certificate = $this->checkForLicense( $clientMachineId )[1];
$license = $this->checkForLicense( $clientMachineId )[0];
$this->logThis( "Machine ".$clientMachineId." received license: {$license}", 5 );
$this->logThis( "Machine ".$clientMachineId." received certificate: {$certificate}", 5 );
$this->logThis( "Machine ".$clientMachineId." received password: {$certpass}", 5 );
$this->logThis( "Machine ".$clientMachineId." received code: {$registerCode}", 5 );
$endTime = microtime( TRUE );
$regTime = $endTime - $startTime;
$this->logThis( "--->Machine ".$clientMachineId." registred in: {$regTime} seconds", 5 );
$certificateReturn = array( "registrationCode" => $registerCode, "certificate" => $certificate, "password" => $certpass, "serial" => $license );
return $certificateReturn;
}
When the client provides PC information (e.g., IP address, MAC address) and basic registration details (e.g., department code), the server responds with a certificate and password. Using this certificate, we were able to bypass all authentication verification logic. With this authentication loophole in hand, we quickly shifted our focus to identifying vulnerabilities that could allow us to gain control of the server. Among the functions accessible with the certificate, we found one that allows file uploads, as shown below.
$machineId = trim($_SERVER["SSL_CLIENT_S_DN_CN"]);
switch ($xmlType) {
case "QuickLogs":
...
case "UploadLogStart":
case "UploadLogPutPacket":
case "UploadLogEnd":
require_once('/opt/eppworker/lib/SoapControllerUploadNew.class.php');
$server->setClass("SoapControllerUploadNew");
mcache_ram('machineId', $machineId);
break;
Below is a portion of the UploadLogPutPacket
handler logic. [1] The parameter uploadId
, which represents the name of the file to be uploaded, is stored in the /var/eppfiles/shadows/temp
directory. Since there is no validation of uploadId
during the file storage process, it is possible to write arbitrary files via a Path Traversal attack.
public function UploadLogPutPacket( $uploadId, $packet = "", $packetCRC = "" )
{
if ( $this->isSoapAuth( ) )
{
$this->logThis( "FileUpload Error: not authenticated" );
return array( "resultCode" => ALLOK );
}
$shadowDir = "/var/eppfiles/shadows";
$shadowTempDir = $shadowDir."/temp";
$uploadFile = $shadowTempDir."/".$uploadId;
if ( isset( $_FILES['upshadow'] ) )
{
$fuploaded = fopen( $_FILES['upshadow']['tmp_name'], "rb" );
$fhandle = fopen( $uploadFile, "a" ); // [1]
$size = 0;
while ( !feof( $fuploaded ) )
{
$data = fread( $fuploaded, 81920 );
$writeOk = TRUE;
$i = 1;
do
{
for ( ; $i <= 3; ++$i )
{
$writed = fwrite( $fhandle, $data );
Since it is possible to create files in arbitrary paths, uploading a web shell to execute arbitrary commands is also feasible. However, the web shell uploaded through this method had limited permissions (www-data
), restricting the execution of certain commands. To address this, we investigated further to identify root daemons and services running on the server with the aim of escalating privileges. During this investigation, we discovered a Worker process responsible for managing log files.
$ ps -ef
root 1037 995 0 Feb22 ? 00:00:00 /bin/sh -c /usr/local/bin/php /opt/eppworker/run_workers.php > /dev/null 2>&1
root 1042 1037 0 Feb22 ? 00:04:11 /usr/local/bin/php /opt/eppworker/run_workers.php
The run_workers.php
script, which runs with root privileges, executes ventilator.php
.
The ventilator.php
script contains logic for managing log files on the file system. Below is a portion of the ventilator.php
code.
$path = $config['logs_path']; // [1]
$tmpFile = uniqid( "list-" );
$cmd = "find ".$path." -type f | grep -v '\\.env'> /tmp/".$tmpFile.".pre";
system( $cmd );
$cmd = "sort /tmp/".$tmpFile.".pre > /tmp/".$tmpFile; // [2]
system( $cmd );
shell_exec( "rm -rf /tmp/".$tmpFile.".pre" );
...
$fr = fopen( "/tmp/".$tmpFile, "r" ); //[3]
while ( $file = fgets( $fr ) )
{
if ( trim( $file ) == "" )
{
if ( 1000000 <= $count )
{
break;
}
else
{
$file = trim( $file );
$dstPath = $workersPath."/".$pos."/".$hourFolder;
$ret = preg_match( "/[0-9a-f]+-(\\d+)/", pathinfo( $file, PATHINFO_BASENAME ), $matches );
}
if ( $ret )
{
}
else
{
$timePath = ceil( time( ) / 30 ) * 30;
$dstPath = $dstPath."/".$timePath;
logthis( "Destination path" );
logthis( $dstPath );
if ( is_dir( $dstPath ) )
{
mkdir( $dstPath );
}
$dstFile = $dstPath."/".pathinfo( $file, PATHINFO_BASENAME );
logthis( "Destination File" );
logthis( $dstFile );
shell_exec( "mv -f ".$file." {$dstFile}" ); // [4]
shell_exec( "mv -f ".$file.".env ".$dstFile.".env" ); // [4]
Let’s take a closer look at how the code functions.
[1] The code first initializes the path
with logs_path
(/var/eppfiles/quicklogs/logs
), as specified in the configuration file.
[2] It then creates a temporary file containing information about all the log files in that directory.
[3] Next, the temporary file is opened to perform move operations on each log file.
[4] Finally, the script sequentially retrieves each log file and uses the shell_exec
function to execute the file move operation. If any file in the log path contains shell metacharacters in its name (e.g., ../../../../../../../var/eppfiles/quicklogs/logs/exploit;ls;
), a Command Injection can occur during the move process.
By exploiting the ability to create files in arbitrary paths through Path Traversal attack, combined with the logic that operates with root privileges, we were able to execute arbitrary commands with root permissions, ultimately gaining control over the EPP server.
After gaining control of the server, we attempted to compromise the client, during which we discovered additional vulnerabilities: CVE-2024–36073, CVE-2024–36074, and CVE-2024–36075.
CVE-2024–36073: Insufficient input validation in shadow function
CVE-2024–36073 is a Command Injection vulnerability in the File shadowing feature of macOS clients. By exploiting this vulnerability, an attacker who has gained administrative access to the server can take control of all devices that have the macOS EPP client installed.
EPP provides a feature that uploads detected files to a designated repository based on policy. This process of uploading detected files to the repository is referred to as File shadowing.
During File shadowing, the EPP server provides options to store files in various locations (e.g., AWS S3, Azure, or the EPP server’s repository). The client determines the file upload method based on the value specified in shadows.server.protocol
received from the server and utilizes the corresponding configuration settings. For instance, if the upload method is Azure
, the client retrieves the FTP URL, port, and account information accordingly. The code is as follows.
epp::OptionsStore::getOptionString(obj->options_store, "ftp.url", &ftp_url, "");
epp::OptionsStore::getSecretOption(obj->options_store, "ftp.credentials", &ftp_cred, 0LL);
std::string::operator=(&shadow_obj, &ftp_url);
shadow_obj.ftp_port = epp::OptionsStore::getOptionInt(obj->options_store, "ftp.port", 0);
shadow_obj.protocol = epp::OptionsStore::getOptionInt(obj->options_store, "shadows.server.protocol", 0);
std::string::operator=(&shadow_obj.ftp_cred, &ftp_cred);
..
if ( shadow_obj.protocol != 4 )
{
if ( shadow_obj.protocol != 3 )
{
v36 = epp::EppServerClient::remoteServerFileUpload(*(v31 + 144), v109, &shadow_obj, &v108);
goto LABEL_77;
}
LABEL_40:
memset(&v76, 0, sizeof(v76));
v35 = epp::EppServerClient::remoteAzureFilesUpload(*(v31 + 144), v109, &shadow_obj, &v106, *(v31 + 176), &v76);
...
epp::OptionsStore::getSecretOption(obj_->options_store, "s3bucket.access_key", &access_key, 0LL);
epp::OptionsStore::getSecretOption(obj_->options_store, "s3bucket.secret_key", &sec_key, 0LL);
epp::OptionsStore::getOptionString(obj_->options_store, "s3bucket.region", ®ion, "");
epp::OptionsStore::getOptionString(obj_->options_store, "s3bucket.name", &name, "");
epp::OptionsStore::getOptionString(obj_->options_store, "s3bucket.path", &path, "");
memset(&v86, 0, sizeof(v86));
epp::InstanceIds::getComputerName(v87, obj_->options_store, &v86, 0LL);
std::string::basic_string(&v75, &v86);
epp::aws_s3_util::UploadFileS3Bucket(&access_key, &sec_key, &name, ®ion, &path, v109, &v110,
&v75, &obj_->field_0[16], v97, __s1);
In the case of Azure
, file uploads are supported through SMB and NFS, but EPP specifically uses SMB for uploading files. Therefore, the client mounts the SMB file system using the mount_smbfs
command before uploading the detected files. Below is the code that generates the command.
std::operator+(&command, "mount_smbfs //", p_ftp_cred);
v25 = std::string::append(&command, "@");
cmd = *v25;
std::string::append(&cmd, ftp_url, ftp_url_len);
std::string::append(&cmd, " ");
strcpy(command.__r_.__value_.__l.__data_, "/private/var/tmp/epp/mount");
epp::file_util::createDirectory(&command, 3LL, 493LL);
cmd_str = cmd.__r_.__value_.__s.__data_;
if ( (cmd.__r_.__value_.__s.__size_ & 1) != 0 )
cmd_str = cmd.__r_.__value_.__l.__data_;
if ( epp::runSynchronousShellCommand(cmd_str) )
The final command generated by the code is as follows:
mount_smbfs //{username}:{password}@{IP}{PATH} /private/var/tmp/epp/mount
As shown below, since the epp::runSynchronousShellCommand
function uses popen
instead of the safer execve
system call, the generated command is vulnerable to Command Injection. This means that by inserting shell metacharacters along with malicious commands into {PATH}
, arbitrary commands can be executed on the PC where the EPP client is installed.
__int64 __fastcall epp::runSynchronousShellCommand(const char *cmd)
{
p_fd = popen(cmd, "r");
if ( p_fd )
{
v12 = 0;
while ( fgets(buf, 1034, p_fd) )
;
v3 = pclose(p_fd);
The following is an example of creating the /tmp/pwnpwn
file using an arbitrary PATH along with the ;
character.
As in the example above, this vulnerability allows an attacker to execute arbitrary commands. By leveraging this, we were able to install backdoors on employees’ devices to establish a persistent access environment for further exploitation.
CVE-2024–36074: Insufficient validation of third-party resource acquisition
CVE-2024–36074 is a Zip Slip vulnerability that occurs during the third-party EasyLock update process on Windows clients.
Zip Slip: An attack method where an attacker create or overwrite arbitrary files in intended paths by including Path Traversal sequences in a compressed file that is then extracted.
By exploiting this vulnerability, an attacker who has obtained administrative access to the server can take control of all devices with the Windows EPP client installed.
EPP provides the EasyLock feature, which encrypts and protects designated media to prevent data leakage. This feature operates through a separate executable, EasyLock.exe
, making its update process independent of EPP agent updates. The update process involves updating a compressed file located on the server at /var/www/EPPServer/sieratool/web/easylock/
, which is then downloaded and extracted by the client.
Below is the code responsible for performing the update.
std::string::string(config_el_url);
epp::OptionsStore2::getOptionString(v4, "el.url", config_el_url, byte_180245130);
std::string::string(config_settings_dir);
epp::OptionsStore2::getOptionString(v4, "settings_dir", config_settings_dir, byte_180245130);
std::string::assign(config_settings_dir, "\\");
std::string::assign(config_settings_dir, "EasyLock.exe");
remove_https_el_url = std::string::c_str(el_url_ + 4);
std::string::operator_eq(assign_el_url, remove_https_el_url);
el_url = std::string::c_str(config_el_url);
if ( std::string::comapre(assign_el_url, el_url)
|| (setting_dir = std::string::c_str(config_settings_dir), _access(setting_dir, 0)) )
{
v331 = debug(
v641,
1,
"C:\\epp-client\\src\\SiEServiceDLL\\ApplicationSettings.cpp",
1036,
"ApplicationSettings::GetMessageListChanged");
v332 = std::string::c_str(assign_el_url);
v333 = std::string::c_str(config_el_url);
epp::LogHelper::operator()(v331, "EasyLockUrl option changed, old value: %s, new value is: %s", v333, v332);
v334 = std::string::c_str(assign_el_url);
process_update(v334);
In the code, the client’s configuration file checks whether the el.url
, which is the URL setting for downloading the EasyLock update file, and settings_dir
, the directory containing the configuration files for the EPP agent, exist on the file system. It also verifies whether these configuration files are accessible. If both conditions are met, the process_update
function is executed to perform the update.
void __fastcall process_update(_BYTE *el_url)
{
if ( el_url )
{
option = get_options_ini();
v4 = -1i64;
el_len = -1i64;
do
++el_len;
while ( el_url[el_len] );
if ( el_len )
{
epp::OptionsStore2::getOptionString(option, "settings_dir", settings_dirs, byte_18023B0C0);
string::copy(el_path, settings_dirs, settings_dirs_len);
if ( v78 == v77 )
{
string::append(el_path, 1ui64, 0i64, "\\", 1ui64);
}
if ( v78 - v77 < 0xD )
{
string::append(el_path, 0xDui64, 0i64, "EasyLocks.zip", 0xDui64);
}
The process_update
function, as shown above, appends EasyLocks.zip
to the settings_dir
value from the options.ini
configuration file to generate the download path.
if ( epp::OptionsStore2::downloadFile(option, download_path, download_url) ) {
...
epp::extractZipFile(path, sett_dir);
...
}
As described above, EPP uses the previously generated download path and URL to download the compressed file via the epp::OptionsStore2::downloadFile
function and then extracts the file using the epp::extractZipFile
function. This process utilizes the open-source libzip
library. Further analysis revealed that the libzip
in use does not properly validate file names during extraction, leaving it vulnerable to the Zip Slip exploit.
By exploiting the Zip Slip vulnerability, an attacker can overwrite or create files in arbitrary paths. To execute a malicious script seamlessly, we decided to overwrite a file that could be executed by an administrator’s command on the agent. We identified a Windows batch file (epp_collect_dpi_info
) used by the EPP agent to compress and send device information (e.g., network, process details) to the server. By replacing this batch file with a PowerShell script designed to download and execute a backdoor, we successfully gained control over employee devices.
CVE-2024–36075: Insufficient input validation in application configuration
CVE-2024–36075 is an Injection vulnerability that occurs when macOS/Windows clients apply settings provided by the server. By exploiting this vulnerability, an attacker with administrative access to the server can take control of all devices with the EPP client installed.
The EPP server manages clients by sending configuration values, which the client stores in the options.ini
file in a key=value
format. This configuration file contains various information, including details for connecting to the server and other settings such as update paths.
Some examples of the specified configuration values are provided below. As we observed in CVE-2024–36073, the EasyLock update process involves downloading files via the URL specified in el.url
. Therefore, if an attacker can manipulate this value, they could download an arbitrary update file.
el.upgrade=1
el.url=https://test.so/index.php/el/softwareEl?ver=2033
el.ver=2.0.3.3
ws_certpass=8728
ws_certpath=/private/etc/epp/cert.pem
ws_port=443
ws_server=test.so
The client uses the epp::OptionsStore::getOptionString
and epp::OptionsStore::setOptionString
functions to retrieve or store values in the configuration file. Below is the code for the epp::OptionsStore2::setOptionString
function, which writes values to the configuration file in the key=value\n
format.
if ( (long_flag & 1) != 0 )
{
key = obj->key;
len = obj->len;
}
else
{
len = long_flag >> 1;
key = obj->gap21;
}
v12 = std::__put_character_sequence>(&file, key, len);
LOBYTE(chr_eq) = '=';
v13 = std::__put_character_sequence>(v12, &chr_eq, 1LL);
long_flag2 = obj->long_flag2;
v15 = obj->short_value;
if ( (long_flag2 & 1) != 0 )
{
value = obj->value;
value_len = obj->value_len;
}
else
{
value_len = long_flag2 >> 1;
value = obj->short_value;
}
v18 = std::__put_character_sequence>(v13, value, value_len);
LOBYTE(chr_) = '\n';
std::__put_character_sequence>(v18, &chr_, 1LL);
Since there is no validation for the configuration values received from the server, special characters (e.g., Newline) can be inserted. This vulnerability allows an attacker to add and apply arbitrary Key-Value pairs.
During the project, we were able to take control of all EPP clients using CVE-2024–36073 and CVE-2024–36074, so we did not utilize this particular vulnerability separately. But CVE-2024–36075 is also useful to take over the clients. This vulnerability could have been exploited to inject a Value into a Key preceding the targeted Key, causing the manipulated Key to be referenced during file loading. This opens up further attack vectors, such as command injection, ultimately enabling an attacker to take control of the client.
Concusion
The vulnerabilities discussed in this post were not merely theoretical; they were discovered and actively exploited during an APT project for a client. Here’s a summary of how we leveraged them:
First, we analyzed the EPP solution identified during the asset identification process and used the file upload vulnerability (CVE-2024–36072) to gain root access to the server.
With root access, we inserted a module to capture all debug logs communicating with the server, allowing us to steal the plaintext credentials (ID/PW) of an administrator who logged in.
Next, we attempted to take control of the clients to access other services or steal sensitive corporate information. By exploiting multiple vulnerabilities (CVE-2024–36073, CVE-2024–36074, CVE-2024–36075), we gained control over both macOS and Windows clients.
Using the credentials obtained earlier, we attempted to decrypt the key management application (e.g., Keychain) on the administrator’s client, successfully decrypting it on some devices. This allowed us to hijack CI/CD sessions, Chrome cookies, and access sensitive information within the company’s messaging and collaboration tools.
Endpoint Protector by CoSoSys, which was covered in this post, is a security solution used by many companies.
While companies adopt solutions with features like employee monitoring and data leakage prevention to enhance security, they must also recognize that these solutions can themselves become attack surfaces. Therefore, it is crucial for companies using external solutions to regularly apply security patches. This is particularly important for closed-source solutions, where direct code modification is not possible.
Even with extensive security measures in place, a single vulnerability can lead to a security incident. For solutions used in-house, network settings should be configured to restrict external access only to the company network when such access is unnecessary. Additionally, to prevent secondary damage from credential theft, it is essential to use unique passwords for each service and implement Multi-Factor Authentication (MFA). Ultimately, by embracing the Zero Trust model’s principle of “Never Trust, Always Verify,” companies can manage their services with greater security.
About Theori Security Assessment
The Security Assessment Team at Theori protects businesses by securing clients’ services and infrastructure through offensive security audits, simulating real hacker tactics. We are passionate about solving complex cybersecurity challenges to create a safer world. As a leader in offensive cybersecurity, we stay ahead of attackers by proactively addressing threats and tackling seemingly impossible problems with a technology-driven approach.