CoSoSys Endpoint Protector에서 발견한 0-day 원격 코드 실행 취약점

Theori Security Assessment 팀이 APT 모의침투 프로젝트에서 CoSoSys Endpoint Protector(EPP)의 4가지 RCE 취약점을 발견했습니다. 해당 취약점을 분석하고 활용하여 서버 및 클라이언트를 장악한 과정, CVE-2024–36072~36075의 상세 내용, 그리고 기업 보안 솔루션의 잠재적 위험과 대응 전략을 알아보세요.
Frontier Squad's avatar
Aug 29, 2024
CoSoSys Endpoint Protector에서 발견한 0-day 원격 코드 실행 취약점

들어가며

Theori Security Assessment 팀(이하 SA 팀)은 고객사를 대상으로 한 APT(지능형 지속 공격, Advanced Persistent Threat) 모의침투 프로젝트 중, CoSoSys의 Endpoint Protector(EPP)를 분석하여 4건의 RCE(원격 코드 실행, Remote Code Execution) 취약점을 발견했습니다. 프로젝트 과정 중 해당 취약점들을 이용하여, EPP 서버와 클라이언트를 모두 장악하고 솔루션을 사용하는 공격 대상의 민감한 정보를 탈취할 수 있었습니다. SA 팀은 안전한 세상을 만드는 것에 기여하기 위해, 프로젝트 종료 후 발견한 모든 취약점의 상세 내용 및 익스플로잇 코드를 솔루션의 벤더사인 Netwrix에 제보했습니다. 해당 취약점들은 아래와 같은 CVE 번호를 부여 받았습니다.

  • CVE-2024–36072 (CVSSv4 10): 서버 애플리케이션의 로그 관련 구성 요소에 존재하는 원격 코드 실행 취약점

  • CVE-2024–36073 (CVSSv4 8.5): 에이전트의 Shadow 기능 관련 구성 요소에 존재하는 원격 코드 실행 취약점

  • CVE-2024–36074 (CVSSv4 7.3): 에이전트의 third-party 리소스(EasyLock) 업데이트 방식에 존재하는 원격 코드 실행 취약점

  • CVE-2024–36075 (CVSSv4 7.2): 에이전트가 서버 설정을 적용하는 과정에 존재하는 원격 코드 실행 취약점

현재 위 취약점들은 모두 패치된 상황입니다. 본 포스트에서는 해당 EPP 솔루션을 분석하게 된 배경 및 분석 과정과 취약점을 이용하여 서버 및 클라이언트를 장악한 방법에 대해 이야기해보고자 합니다. 빠르게 패치를 진행하고, 취약점을 공개하는 것을 허락한 Netwrix에 감사드립니다.


분석 배경

SA 팀은 고객사의 내/외부 보안성 향상 및 임직원 보안의식 증진을 목적으로 하는 APT 모의침투 프로젝트를 진행했습니다. 공격의 목표는 고객사의 민감 정보 및 내부 자산을 최대한 많이 획득하는 것이었고, 따라서 내부 침투를 위한 첫 단계는 자산 식별이었습니다.

자산 식별 과정에서, 고객사가 CoSoSys의 EPP 솔루션을 사용한다는 사실을 파악했습니다. 이와 같은 전사에 적용되는 솔루션은 중앙 소프트웨어를 이용하여 임직원의 기기를 제어할 수 있기 때문에, 측면 이동(Lateral Movement) 공격에 유용합니다. 측면 이동은 기업의 자산 탈취에 있어서 매우 중요한 역할을 하기 때문에, 저희는 해당 소프트웨어를 대상으로 공격을 계획했습니다.

우선적으로, 과거에 발견된 취약점들을 이용할 수 있을지 찾아보았습니다. 하지만, CoSoSys EPP와 관련된 공개된 취약점이 발견된 것은 2019년이 마지막이었고, 따라서 과거의 취약점들을 활용하기는 어려웠습니다.

따라서, 저희는 새로운 취약점을 발견하기 위해 해당 솔루션의 서버와 클라이언트를 대상으로 취약점 분석을 수행했습니다. EPP 서버의 소스 코드는 PHP 언어로 개발되었으며, 코드는 Zend Guard를 통해 보호되고 있었습니다. 따라서, 공개된 Dezend 도구를 이용하여 원본 소스 코드를 획득하고 취약점 분석을 수행해야 했습니다. 또한, macOS/Windows 클라이언트는 C++ 언어로 개발되어 있었는데, 소스 코드를 바로 획득할 수 없어 정적/동적 분석을 활용한 리버스 엔지니어링(리버싱)이 필요했습니다.


CoSoSys Endpoint Protector

CoSoSys EPP는 조직에서 다루는 매체가 외부로 유출되는 것을 방지하고, 유출 경로를 추적하기 위한 Data Loss Prevention(DLP) 솔루션의 일종입니다. EPP를 사용할 경우, On-premise 형태의 관리 서버를 제공받아 정책 설정을 통해 서버와 연결된 클라이언트를 검열할 수 있습니다. 정책은 솔루션 관리자가 직접 생성하여 클라이언트에 배포할 수 있는데, 관리자는 정책을 통해 기업에서 다루는 민감 정보(예: 문서, 이미지, 소스 코드) 중 어떤 자산의 이동을 추적할 것인지 정의할 수 있습니다. 또한, EPP는 매체 이동의 가능성이 있는 모든 엔드포인트(예: 공유 폴더, USB, 이메일, 메신저)에서 자료가 유출되는 것을 사전에 차단하는 기능 및 자료의 이동을 추적하는 기능을 제공합니다.


그럼 이제부터 APT 공격에 사용했던 EPP 솔루션의 취약점들을 자세히 살펴보겠습니다.


CVE-2024–36072: Insufficient input validation in file upload

가장 먼저 살펴볼 취약점은 CVE-2024–36072입니다. 해당 취약점은 인증되지 않은 이용자가 임의 경로에 파일을 업로드하여 서버의 루트 쉘(Shell)을 획득하는 RCE 취약점입니다.

취약점 분석을 위해 솔루션의 구조 분석을 먼저 진행했고, EPP 솔루션의 주요 로직이 인증된 이용자를 대상으로 제공된다는 것을 파악했습니다. 이어서, 이용자 인증을 우회할 수 있거나 인증 없이 공격 가능한 취약점을 찾기 위해 인증 방식 및 인증 여부를 확인하는 로직을 분석했습니다. 확인 결과, 솔루션에서 인증이 필요한 기능은 아래와 같이 isSoapAuth 함수를 이용하여 인증 여부를 검증하고 있었습니다.

if ( $this->isSoapAuth( ) )

해당 함수는 SSL 통신을 시도하는 클라이언트의 인증서 정보인 서버 변수의 SSL_CLIENT_S_DN_CN 필드를 가져오고, 인증서의 서버 DB(데이터베이스, Database) 등록 여부 및 라이센스 유무를 판단하여 인증된 이용자를 식별합니다. 아래는 함수의 코드입니다.

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;
    }
    ...
}

인증을 우회하기 위한 목적으로, 인증 없이 진행되는 통신의 메시지 처리 과정을 확인한 결과, SOAP 프로토콜을 이용하는 다양한 핸들러를 발견할 수 있었습니다. 그중에는 이용자가 전달한 파라미터를 기반으로 인증서를 발급해주는 역할을 하는 Register 핸들러도 있었습니다. 아래는 Register 핸들러의 코드입니다.

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;
}

PC 정보(예: IP, MAC 주소) 및 기본적인 가입을 위한 정보(예: 부서 코드)를 파라미터로 전달하면, 서버는 인증서와 패스워드를 반환합니다. 여기서 반환한 인증서 정보를 이용하면, 인증 상태를 검증하는 모든 로직을 이용할 수 있습니다. 이렇게 인증에 대한 문제를 해결한 후, 저희는 곧바로 서버 장악을 위한 취약점을 탐색했습니다. 인증서를 이용해 접근할 수 있는 기능 중에는 아래와 같이 파일을 업로드할 수 있는 기능이 존재했습니다.

$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;

아래는 UploadLogPutPacket 핸들러 로직의 일부입니다. [1] 부분의 코드를 살펴보면, 업로드할 파일의 이름을 의미하는 파라미터인 uploadId/var/eppfiles/shadows/temp 디렉터리에 저장하고 있습니다. 파일 저장 과정에서 uploadId를 검사하지 않기 때문에, Path Traversal을 통한 임의 파일 쓰기가 가능합니다.

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 );

임의 경로에 원하는 파일을 생성할 수 있기 때문에, 웹쉘 업로드를 통해 임의 명령어를 실행하는 것도 가능합니다. 하지만, 해당 방법으로 업로드한 웹쉘은 권한이 낮아 명령어 실행에 제약이 있습니다. 이에, 저희는 권한 상승을 목적으로 서버에서 동작하는 루트 데몬 및 서비스를 추가로 확인했습니다. 그 결과, 로그 파일을 관리하기 위한 Worker가 동작하고 있는 것을 발견했습니다.

$ 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

루트 권한으로 동작하고 있는 run_workers.php 코드는 ventilator.php를 실행합니다.

ventilator.php는 파일 시스템에 존재하는 로그 파일을 관리하기 위한 로직을 포함하고 있습니다. 아래는 ventilator.php 코드의 일부입니다.

$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]

코드의 동작을 살펴보겠습니다.

[1]을 보면, 코드는 먼저 설정 파일에 존재하는 logs_path/var/eppfiles/quicklogs/logs 경로를 초기화합니다. 이후 [2]에서 해당 경로에 존재하는 모든 로그 파일에 대한 정보를 담고 있는 임시 파일을 및 생성합니다. 그 다음으로 [3]에서 각 로그 파일에 대한 이동 작업을 수행하기 위해 임시 파일을 열고, [4]에서 로그 파일을 순차적으로 가져와 shell_exec 함수를 통해 파일 이동 작업을 수행합니다. 이때, 로그 경로 위치에 존재하는 파일이 이름에 쉘 메타 문자를 포함하는 경우(예: ../../../../../../../var/eppfiles/quicklogs/logs/exploit;ls;), 이동하는 과정에서 Command Injection이 발생합니다.

이렇게 Path Traversal을 통해 임의 경로에 파일을 생성할 수 있는 문제점과 루트 권한으로 동작하는 로직을 함께 활용하여 루트 권한으로 임의 명령어를 실행하여 EPP 서버를 장악할 수 있었습니다.


서버 장악 이후 클라이언트 장악을 위한 시도 중, 나머지 취약점들(CVE-2024–36073, CVE-2024–36074, CVE-2024–36075)을 발견했습니다.


CVE-2024–36073: Insufficient input validation in shadow function

CVE-2024–36073은 macOS 클라이언트의 File shadowing 기능에서 발생하는 Command Injection 취약점입니다. 해당 취약점을 이용하면, 서버의 관리자 권한을 획득한 공격자가 macOS용 EPP 클라이언트가 설치된 모든 기기를 장악할 수 있습니다.

EPP는 정책에 따라 탐지된 파일을 지정된 저장소에 업로드하는 기능을 제공합니다. 이때 탐지된 파일을 저장소에 업로드 하는 행위를 File shadowing이라고 합니다.

File Shadowing 정책 설정 화면
File Shadowing 정책 설정 화면

File shadowing 시 EPP 서버는 다양한 위치(예: AWS S3, Azure, EPP 서버의 저장소)에 파일을 저장하는 옵션을 제공합니다. 클라이언트는 서버에서 내려받은 정보 중, shadows.server.protocol에 명시된 값에 따라서 파일 업로드 방식을 결정하고, 방식에 따른 설정 값을 활용합니다. 예를 들어, 파일 업로드 방식이 Azure라면 그에 따른 FTP URL, 포트, 계정 정보를 가져옵니다. 코드는 아래와 같습니다.

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);

Azure의 경우 SMB와 NFS를 통해 파일 업로드를 지원하는데, EPP에서는 SMB를 이용하여 업로드를 수행합니다. 따라서, 클라이언트는 mount_smbfs 명령어를 이용해 SMB를 파일 시스템에 마운트한 후 탐지된 파일을 업로드합니다. 아래는 명령어를 생성하는 코드입니다.

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) )

코드를 통해 최종적으로 생성될 명령어는 아래와 같습니다.

  • mount_smbfs //{username}:{password}@{IP}{PATH} /private/var/tmp/epp/mount

epp::runSynchronousShellCommand 함수는 아래와 같이 Command Injection을 방지할 수 있는 execve 시스템 콜이 아닌 popen을 사용하고 있기 때문에, 앞서 생성된 명령어를 이용한 Command Injection이 가능합니다. 따라서, {PATH}에 쉘 메타 문자와 함께 악의적인 명령어를 삽입하면 임의의 명령어를 EPP 클라이언트가 설치되어 있는 PC에서 실행할 수 있습니다.

__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);

아래는 임의의 PATH와 함께 ; 문자를 활용하여 /tmp/pwnpwn 파일을 생성하는 예시입니다.

Folder Path 설정에 /tmp/pwnpwn 파일을 생성하는 공격 코드를 삽입한 모습
Folder Path 설정에 /tmp/pwnpwn 파일을 생성하는 공격 코드를 삽입한 모습

예시와 같이, 해당 취약점을 통해 공격자는 임의의 명령어를 실행할 수 있는 권한을 획득할 수 있습니다. 이를 이용하여 임직원의 기기에 백도어를 설치함으로써 지속적인 접근이 가능한 환경을 구축할 수 있었습니다.


CVE-2024–36074: Insufficient validation of third-party resource acquisition

CVE-2024–36074는 Windows 클라이언트의 third-party 리소스인 EasyLock 업데이트 과정에서 발생하는 Zip Slip 취약점입니다.

Zip Slip: Path Traversal 구문이 포함된 파일이 담긴 압축 파일을 압축 해제할 때 공격자가 의도한 경로에 임의 파일을 생성하거나 덮어쓰는 공격 방법

해당 취약점을 이용하면, 서버의 관리자 권한을 획득한 공격자가 Windows용 EPP 클라이언트가 설치된 모든 기기를 장악할 수 있습니다.

EPP는 정보 유출 방지를 목적으로 지정한 매체를 암호화하여 보호할 수 있는 EasyLock 기능을 제공하는데, 이는 EasyLock.exe 이름을 가진 별도의 실행 파일을 이용합니다. 따라서, EasyLock 업데이트는 EPP 에이전트 업데이트와 별도로 수행됩니다. 업데이트는 서버에서 /var/www/EPPServer/sieratool/web/easylock/ 경로에 있는 압축 파일을 업데이트하고, 클라이언트에서 해당 압축 파일을 다운로드하여 압축을 해제하는 방식으로 이루어집니다.

아래는 업데이트를 수행하는 코드입니다.

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::compare(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);

코드를 보면, 클라이언트의 설정 파일에서 EasyLock 업데이트 파일을 다운로드할 URL 설정 값인 el.url과 EPP 에이전트의 설정 파일을 담고 있는 디렉터리인 settings_dir 이 파일 시스템에 존재하는지 검증하고 설정 파일 접근 가능 여부를 판단합니다. 이 둘을 모두 만족할 경우, process_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);
      }

process_update 함수는 위와 같이 options.ini 설정 파일의 settings_dir 값의 뒤에 EasyLocks.zip을 붙여 다운로드 경로를 생성합니다.

if ( epp::OptionsStore2::downloadFile(option, download_path, download_url) ) {
    ...
    epp::extractZipFile(path, sett_dir);
    ...
}

위와 같이, EPP는 앞서 생성한 다운로드 경로와 URL을 기반으로 epp::OptionsStore2::downloadFile 함수를 통해 압축 파일을 다운로드하고 epp::extractZipFile 함수를 통해 압축 파일을 해제합니다. 이 과정에는 libzip 오픈 소스가 사용됩니다. 추가적인 분석 결과, 사용하는 libzip 소스 코드가 압축 해제 과정에서 파일 이름에 대한 검사를 수행하지 않아 Zip Slip에 취약하다는 것을 알 수 있었습니다.

Zip Slip 취약점을 이용하면 임의 경로에 원하는 파일들을 덮어쓰거나 생성할 수 있습니다. 스크립트를 자연스럽게 실행하기 위해, 관리자가 에이전트에 명령을 내려 실행할 수 있는 파일을 덮어 씌우기로 결정했습니다. 최종적으로, EPP 에이전트에서 사용하는 파일들을 확인하여 기기의 정보(예: 네트워크, 프로세스)를 압축해서 서버로 전달하기 위한 Windows 배치 파일(epp_collect_dpi_info)을 찾아, 해당 파일을 Powershell 스크립트를 이용해 백도어를 다운로드하고 실행하는 스크립트로 조작하여 덮어 씌움으로써 임직원 기기를 장악할 수 있었습니다.


CVE-2024–36075: Insufficient input validation in application configuration

CVE-2024–36075는 macOS/Windows 클라이언트가 서버에서 내린 설정을 적용하는 과정에서 발생하는 Injection 취약점입니다. 해당 취약점을 이용하면, 서버의 관리자 권한을 획득한 공격자는 EPP 클라이언트가 설치된 모든 기기를 장악할 수 있습니다.

EPP 서버는 클라이언트를 관리하기 위한 설정 값을 내려주고, 클라이언트는 해당 값을 options.ini 설정 파일에 key=value 형태로 값을 저장합니다. 해당 설정 파일은 서버와 연결하기 위한 정보 외에도 다양한 정보(예: 업데이트 경로)를 포함하고 있습니다.

명시된 설정 값 일부의 예시는 아래와 같습니다. CVE-2024–36073에서 살펴봤던 것처럼, EasyLock 업데이트 시 el.url에 명시된 URL을 통해 파일을 다운로드하기 때문에 해당 값을 조작할 수 있다면 임의의 업데이트 파일을 다운로드할 수 있습니다.

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

클라이언트는 epp::OptionsStore::getOptionStringepp::OptionsStore::setOptionString 함수를 통해 설정 파일의 값을 불러오거나 저장합니다. 아래는 epp::OptionsStore2::setOptionString 함수의 코드로, key=value\n 형태로 설정 파일에 값을 쓰는 것을 확인할 수 있습니다.

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);

이때 서버에서 내려받은 설정 값에 대한 검사가 존재하지 않아 특수문자(예: Newline)를 입력할 수 있습니다. 이를 이용하면, 임의의 Key에 임의 Value를 추가하고 적용하는 것이 가능합니다.

프로젝트 과정 중에는 CVE-2024–36073, CVE-2024–36074를 이용하여 모든 EPP 클라이언트를 장악할 수 있었기에 해당 취약점을 별도로 활용하지는 않았습니다. 하지만 CVE-2024–36075를 이용하여 조작하려는 Key보다 앞선 위치에 있는 Key에 Value를 Injection하면, 실제 파일을 불러오는 과정에서 조작된 Key를 참조하므로 추가적인 공격(예: Command Injection)을 통해 마찬가지로 클라이언트 장악이 가능합니다.


결론

SA 팀은 고객사를 대상으로 한 APT 프로젝트에서 앞서 살펴본 취약점들을 실제로 발견하고 이용했습니다. 전체적인 활용 방식은 아래와 같습니다.

EPP 취약점 이용 방식
EPP 취약점 이용 방식

  1. 가장 먼저, 자산 식별 과정에서 발견된 EPP 솔루션을 분석하고 파일 업로드 취약점(CVE-2024–36072)를 이용하여 서버의 루트 권한을 획득했습니다.

  2. 이후, 해당 취약점을 이용하여 서버와 통신하는 모든 디버그 로그를 기록할 수 있는 모듈을 삽입하여 로그인하는 관리자의 평문 ID/PW를 탈취했습니다.

  3. 다음으로는, 타 서비스에 접근하거나 기업의 민감 정보를 탈취하기 위한 목적으로 클라이언트 장악을 시도했고, 여러 취약점(CVE-2024–36073, CVE-2024–36074, CVE-2024–36075)을 이용한 결과 macOS/Windows 클라이언트 모두를 장악할 수 있었습니다.

  4. 이전 단계에서 획득한 ID/PW를 이용하여 관리자의 클라이언트에서 키 관리 애플리케이션(예: Keychain) 대상 복호화를 시도했고, 일부 기기에서 복호화에 성공했습니다. 이를 통해 CI/CD 세션 및 Chrome Cookie를 탈취하였고, 기업 메신저 및 협업 도구에 침투하여 내부의 민감 정보를 열람할 수 있었습니다.

이번 포스트에서 다룬, 취약점이 발견되었던 CoSoSys Endpoint Protector는 수많은 기업이 사용하고 있는 보안 솔루션입니다.

EPP를 사용하는 기업 수 및 주요 국가
EPP를 사용하는 기업 수 및 주요 국가

기업에서는 보안성 증진을 위해 임직원 제어, 자산 유출 방지를 비롯한 다양한 기능의 솔루션을 도입하여 사용합니다. 하지만, 기업의 보안성 증진을 목적으로 도입한 솔루션 역시, 하나의 공격 표면이 될 수 있다는 것을 인지해야 합니다. Closed source 형태의 솔루션의 경우, 코드를 직접 수정하는 작업이 불가능하기 때문에, 정기적으로 솔루션의 보안 패치를 확인하고 빠르게 적용하는 것이 더욱 중요합니다.

보안을 강화하기 위해 많은 노력을 기울여도, 하나의 약한 지점에서 보안 사고는 발생할 수 있습니다. 사내에서 사용하는 솔루션은 외부 접근이 불필요한 경우 사내망에서만 접근할 수 있도록 네트워크 설정을 적용해야 하며, 계정 정보 탈취로 인한 2차 피해를 방지하기 위해 서비스 별로 비밀번호를 다르게 설정하고 다중 인증을 적용할 필요가 있습니다. 결국, ‘Never Trust, Always Verify’라는 제로트러스트의 아이디어를 기반으로 서비스를 구성하는 다양한 요소들 각각에 대한 보안을 고려해야, 서비스는 더 안전하게 관리할 수 있을 것입니다.


About Theori Security Assessment

티오리 Security Assessment 팀은 실제 해커들의 오펜시브 보안 감사 서비스를 통해 고객의 서비스와 인프라스트럭처를 안전하게 함으로써 비즈니스를 보호합니다. 특히, 더욱 안전한 세상을 위해 난제급 사이버보안 문제들을 해결하는 것을 즐기며, 오펜시브 사이버보안의 리더로서, 공격자보다 한발 앞서 대응하고 불가능하다고 여겨지는 문제를 기술중심적으로 해결합니다.


References

Share article

Theori © 2025 All rights reserved.