Flatt Security Blog

株式会社Flatt Securityの公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

株式会社Flatt Securityの公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

発見した0dayについての技術的解説 - EC-Cube, SoyCMS, BaserCMS

※このブログは、セキュリティエンジニアのstyprが英語で書いた文章を翻訳し、社内で監修したものになります。

f:id:flattsecurity:20201026121415p:plain

こんにちは。株式会社Flatt Securityのstypr (@stereotype32)です。

以前公開した記事「Flatt Securityは“自分のやりたいことが実現できる”場所/セキュリティエンジニア stypr」でお話した通り、私は現在Flatt Securityで0day huntingとセキュリティの診断をしています。

私は 5月に入社してから 0day hunting の業務に取り組んでいます。他の面白い業務と並行して取り組んでいるので、一つの製品にかけている時間は、最低1日からせいぜい1週間程度です。

結果、これまでの間、私はかなり多くのOSS脆弱性を見つけてきました。そこで今回の記事では、現在までに修正された脆弱性のうち、技術的に面白い選りすぐりのものを解説します。なお、未だ未修正の脆弱性など、今は紹介できない脆弱性についても、準備が整い次第追加のブログ記事で紹介する予定です。

まず最初のターゲットとして、日本国内でよく使用されているオープンソースの製品を中心に調査を行いました。 なぜ日本国内のOSSにターゲットを絞ったかというと、日本で開発されたオープンソース製品がとても多く、テストがしやすいと判断したからです。

本記事では、現在までに報告した脆弱性の一部を紹介しています。 報告時に実際に悪用可能であることを証明するために証明コード(以下、PoC)を作成しましたが、各脆弱性の危険性を考慮してPoCコードは掲載せず、PoCコードを実行した際に録画した映像に置き換えました。ご了承ください。

脆弱性の報告を行う過程で適切に対応してくださった各製品の開発者の方々とJPCERT/CC担当者の方、そしてゼロデイを報告する過程で多大な協力をしてくれた社内の皆に心より感謝申し上げます。

BaserCMS

BaserCMSとは

github.com

BaserCMSは、自由にウェブサイトを作成できる国産オープンソースCMSプラットフォームです。 CakePHPで実装されたCMSで、数年間継続的に開発が続けられており、多くのサイトで使用されています。

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

前提条件

この脆弱性は、以下のような前提条件が必要となります。

  • BaserCMS 上で管理者権限を持つユーザを、攻撃者が罠ページに誘導できること
脆弱性解説

まず、「Cross-site Scripting (XSS)」について説明します。一般に、XSS脆弱性は、ユーザの入力を正しくsanitizeしないこと等によって生じます。そのため、XSS脆弱性を探すには、まずsanitizeを正しく行っていない箇所がないかを確認する形のアプローチがよいかと思います。

たとえば、 app/webroot/theme/admin-third/ThemeFiles/admin/index.php:48 を見ると、次のようにcurrentPathをechoしています。

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

currentPathを参照するファイルを追跡してみると、 lib/Baser/Controller/ThemeFilesController.php:171 でassignされますが、名前の通りcurrentPathは現在のディレクトリとファイル名を意味していて、入力されたpathに対して特にsanitizeをしていないためXSSが発生します。

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 ファイルがサーブされる URL が分かるなら、攻撃者は PHP ファイルを任意に実行させることができます。

lib/Baser/Plugin/Uploader/Controller/UploaderFilesController.php:291を見ると以下のようなコードがあります。

 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を確認(lib/Baser/Plugin/Uploader/Model/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);
...
    }
}

確かに拡張子検証をしていますが、管理者アカウントでない場合のみ検証が行われています。 つまり、管理者への XSS 攻撃が成功した場合、ファイル名の検証を回避しながら管理者アカウントから任意のPHPスクリプトをインジェクトし、実行することができます。

PoC / 参考

JavaScriptでファイルのアップロードが必要な場合、FormDataクラスで Blob Object (https://developer.mozilla.org/ja/docs/Web/API/Blob) を利用するとファイルのアップロードが可能です。

これにより、XSS によって任意のスクリプトが実行可能な状況で任意のファイルを追加してアップロードすることができます。

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

https://nvd.nist.gov/vuln/detail/CVE-2020-15159:

github.com

EC-CUBE

EC-CUBEとは

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

github.com

EC-CUBEは日本で最もよく使われているECサイトのオープンソースです。

Symfonyをベースに開発されているプロダクトで、継続的に開発されています。 次のターゲットを決めている途中で、弊社のudonさんが薦めてくださったので短期間ではありますが調査してみました。

f:id:flattsecurity:20201021204522p:plain

Unauthenticated/Authenticated Remote Code Execution (RCE)

前提条件

この脆弱性は、以下のような前提条件が必要となります。

  1. Unauthenticated RCE を試みる場合 … 環境がAPP_DEBUG=1 であること。* Dockerを用いた一般的なセットアップ方法で再現が可能です。

  2. Authenticated RCE を試みる場合 … EC-CUBE 上で管理者権限を持つユーザを、攻撃者の罠ページに誘導できること。

脆弱性解説

まず Unauthenticated RCE について説明します。APP_DEBUG=1 という前提条件が必要ですが、基本的にEC-CUBEの公式インストールガイド にしたがって、Dockerを用いてインストールすると、DEBUGモードは自動的に有効化されます。

$ # 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/に接続します。

f:id:flattsecurity:20201021204553p:plain

上のスクリーンショットをよく見ると、右下に sfというロゴが見えます。 このロゴは、主にSymfonyのデバッグモードが有効化されているときに表示されます。 しかし、ロゴが表示されない場合もありますので、URLに /_profiler/を追加して接続すると、次のようなページが出てきます。

f:id:flattsecurity:20201021204605p:plain

上記はSymfony Profilerという機能で、関連資料がなかなか出てきません。 この機能について簡単に要約して説明すると、Symfonyで開発する製品で問題が発生したときにデバッグを便利にしてくれるツールです。 もちろん、この機能はデバッグモードが有効になったときに使用できます。

Symfony自体はかなり安全なフレームワークですが、この機能が有効になるとセキュリティ上に脆弱になります。 例えば、Profilerには下のようにProfile Searchという機能があります。

f:id:flattsecurity:20201021204624p:plain

上記にもあるように、EC-CUBE に届いたリクエストのログを閲覧することができます。 Tokenをクリックすると、次のスクリーンショットのページが出てきます。 ここをよく見ると、POSTパラメータに対する値が読み取れます。 この機能を通じて管理者及びユーザーのアカウント情報を奪取することができます。

f:id:flattsecurity:20201021204637p:plain

これで、上記の脆弱性を利用して管理者アカウントで正常にログインできると仮定し、Remote Code Executionについて説明します。 管理者としてログインしてサイトを見回してみると、ファイル管理という機能があります。

f:id:flattsecurity:20201021204700p:plain

前述のBaserCMSで説明した場合と同様に、 一般的なPHPプロダクトの場合、PHPファイルを任意にアップロードでき、かつアップロードされたPHP ファイルがサーブされる URL が分かるなら、攻撃者は PHP ファイルを任意に実行させることができるのでした。

src/Eccube/Controller/Admin/Content/FileController.php:52を確認してみると、ある程度のセキュリティ措置は行っていますが、($jailNowDirなどを確認すればわかります)実際に拡張子に対する確認はせず、そのままファイルを保存してしまっています。

    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、重要ファイルのみしか確認していません。

.htaccess

<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フォルダのディレクトリ、管理者のID、PWを横取りします。 パケットがない場合、基本的にadminのアカウントはadmin/passwordです。

  2. 横取りしたアカウント情報でadminとしてログインし、PHPファイルをアップロードします。

  3. html/user_dataにアップロードされたファイルを直接URLに読み込むとPHPコードが実行されます。

その攻撃が作動するかに関するPoCは、以下の映像をご覧になってください。

対応

実際にこの脆弱性について開発者と長期間意見を交わし、開発者が各所への問い合わせや会議を行った結果、開発者は脆弱性と認めないと判断しました。 理由は次の通りです。

  1. PHPファイルが管理者権限でアップロードされる点は、EC-CUBEの独自仕様

  2. 初期設定がデバッグモードになる点は製品版では発生しないため、脆弱性ではないという判断

  3. すでに対応措置のために環境設定に関するガイド共有、セキュリティチェックリスト、セキュリティ関連プラグインの共有及び更新を続けている。

    *https://www.ec-cube.net/products/detail.php?product_id=2040

    *https://doc4.ec-cube.net/environmental_setting

現在、記事作成時点でインターネット上にその脆弱性の影響を受けるウェブサイトが多数あり、EC-CUBEチームもこの状況を把握している状態です。

結局、EC-CUBE開発会社が適切な対応を着実に行っていくことにし、情報提供した脆弱性はPoCを除くある程度の情報を公開できるという条件の下でCloseしました。

この脆弱性に対応するための現状の対応としては、localhost を除くどの環境でもdebug がアクティブにならないように何度も確認する方法だけになっています。

github.com

情報提供でやり取りされた内容に対する詳しい情報は私の個人ブログで作成しましたので、気になる方はこちらでご確認ください。

blog.harold.kim

SoyCMS

SoyCMSとは

https://saitodev.co/soycms/

github.com

SoyCMSはブログとインターネットショッピングモールを構築できる自由度の高いCMS(コンテンツ管理システム)です。 オープンソースのソフトウェアで公開しているので、無料で利用することができます。

インターネットで検索中に偶然発見した製品ですが、名前が好きだったのと、継続的に開発されている製品だと判断して挑戦してみることにしました。

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

前提条件

この脆弱性は、以下のような前提条件が必要となります。

SoyCMS 上で管理者権限を持つユーザを、攻撃者の罠ページに誘導できること。

脆弱性解説

まず、「Cross-site Scripting」について説明します。

コードを分析してみるとcms/app/webapp/inquiry/admin.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();

ページが存在しない場合は$classPathをすぐにリターンする処理を見て怪しさを感じ、色々試してみた結果XSSを発見しました。根本的な原因を探すために、この関数を呼び出しから出力するまでのルーチンを順番にtraceしてみました。

プログラムが実行される時、最初にapp/index.phpCMSApplication::run() が実行されます。

<?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.class.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"]は、現在実行しているファイルのpathです。たとえば、http://hoge.com/index.php/inquiry/blah だと $pathinfo/1/2 になります。

  2. $pathinfo を通じて$self->arguments , $self->applicationId などに値が追加されます。

  3. 入力した $self->applicationIdを通じて $base . "inquiry/admin.php"がincludeされます。

    1. SOYInquiryApplicationのコードの最下段を見ると、$app = new SOYInquiryApplication(); $app->init();が実行されます。

    2. $app->init() が実行される過程を見ていると、CMSApplication::main(array($this,"main"));を呼び出すことがわかります。

      • CMSApplication::main(array(SoyInquiryApplication,”main”)); を実行すると $obj->appMain = Array(SoyInquiryApplication, ”main”);となります。
    3. 最後に$self->application = call_user_func($self->appMain);を通じて SoyInquiryApplication->main()を呼び出します。

      • classは$arguments = CMSApplication::getArguments();を使って$classPathを生成します。
      • しかし、先ほど説明した通り存在しないページの場合、return $classPathがあるのですぐリターンします。
    4. 結局、最終的には$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 はデフォルトのテンプレートです。 このテンプレートのソースコードを少し見ると次のようになります。

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

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

ここでCMSApplication::printApplication()を見ると次のようなコードが見られます。

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

そうです。先ほど説明した通り、$self->application = $classPathですから、悪意のあるパスを入力すると、そのままHTMLタグが入力され、これによってJavascriptを実行できます。

f:id:flattsecurity:20201021204729p:plain

f:id:flattsecurity:20201021204750p:plain

続けて「Remote Code Execution」です。 この脆弱性に「leading to RCE」と言及しましたが、これはまだパッチされていない既存のイシューを利用して攻撃しました。

github.com

f:id:flattsecurity:20201021204802p:plain

管理者の権限で任意にindex.phpソースコードを修正できる問題点が発見されましたが、この問題点がまだパッチされていないようで、この脆弱性を利用してRCEを成功させることができました。

対応

単独で開発を担当していらっしゃるようですので、直接PRをお願いしてコード修正を行いました。

RCEはReferer検査とCSRFトークンを活用して修正しました。

github.com

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

前提条件

この脆弱性は、以下のような前提条件が必要となります。

  • SoyCMS 上で管理者権限を持つユーザを、攻撃者の罠ページに誘導できること。

  • あるいは、管理者に対する XSS 攻撃ができること( CVE-2020-15183を活用することができます)。

脆弱性解説

前述のXSSを使用したもう一つのRCE です。

elFinderで発生しうる問題点が実際に発見されたという点が興味深かったです。

SoyCMSにある soycms/js/elfinder/php/connector.php:143-159を確認してみると次のようなオプションがあります。

$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を追加し、上記のような現象をなくすことです。

         '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は、次の映像で代替します。

対応

単独で開発を担当していらっしゃるようですので、直接PRをお願いしてコード修正を行いました。

github.com

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

前提条件

この脆弱性は、以下のような前提条件が必要となります。

SoyCMS 上で管理者権限を持つユーザを、攻撃者の罠ページに誘導できること。

脆弱性解説

まずコードを少し探してみると cms/app/webapp/inquiry/pages/Template/EditPage.class.php:7-26次のようなコードを見ることができます。

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

上記コードのdoPostメソッドでは、CSRFトークンを検証することもなく、そのままファイルを入力してしまいます。 これにより、外部URLから本人サイトにアクセスするとCSRFを通じてサーバーに悪意のあるファイルを強制的に保存することができます。

その他に、該当ページで上位ディレクトリのファイルをリスティングする脆弱性が__constructにありましたが、これは別途CVEを登録せずにそのままパッチしました。

PoC

攻撃が作動するのかに対するPoCは、以下の映像をご覧ください。

対応

単独で開発を担当していらっしゃるようですので、直接PRをお願いしてコード修正を行いました。

github.com

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

前提条件

この脆弱性は、以下のような前提条件が必要となります。

  • 基本的な内蔵プラグインであるSoy Inquiryの有効化

ほとんどのSoyCMS を使用しているサイトでは、Soy Inquiry を使用しており、実際にこの脆弱性により攻撃可能であることがPoC によって証明されているため、実際の影響力は報告した4つの脆弱性のうち最も高いものとなります。

脆弱性解説

まず基本プラグインであるSoy Inquiryが正常に作動すると仮定し、 cms/app/webapp/inquiry/page.php:126-133を分析すると次のようなコードが出てきます。

     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"]はユーザがコントロールできるため、何の保護もできていません。

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

最も危険なコードです。 PHPの公式ドキュメント(https://www.php.net/manual/ja/function.unserialize.php) を参考にすると、次のような内容があります。

allowed_classesoptions の値にかかわらず、 ユーザーからの入力をそのまま unserialize() に渡してはいけません。 アンシリアライズの時には、オブジェクトのインスタンス生成やオートローディングなどで コードが実行されることがあり、悪意のあるユーザーがこれを悪用するかもしれないからです

あの言葉の通り、 unserialize()は絶対に実際使うコードにあってはならない危険なコードです。インターネットで調べると次のようにunserialize()に対する危険性と対策が紹介されています。

https://www.hack.vet/entry/20170328/1490712989

https://blog.tokumaru.org/2015/07/phpunserialize.html

https://blog.ohgaki.net/how-to-use-php-serialize-unserialize-with-security

Soy Inquiryのフォームでunserialize()を呼び出す前に、すでに宣言されているクラスがたくさんあります。その中でも危険に見えるクラスをうまく活用して任意のコードを実行することができました。

PoCおよび参考

私の場合、PoCを作成するために以下のようにコードを作成し、次のような形のPHPコードから任意のPHPコードを実行することができました。 これよりもっと短いペイロードを作成できると思うので、挑戦してみたい方は一度挑戦してみてください。

<?php

class ClassHoge2 {
...
}

class ClassHoge1 {
...
}

$exploit = serialize(Array(
    "column_1" => ...ClassHoge1,
    "column_2" => "b@b.com",
    "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に関するTipsをお伝えすると、unserializeを効率的にexploitする方法としては、実際にunserialize() コマンドが実行される直前にvar_dump(get_declared_classes()); のようなコードを追加すると、効率的に見つけることができます。

攻撃が作動するのかに対するPoCは、以下の映像でご覧ください。

対応

単独で開発を担当していらっしゃるようですので、直接PRをお願いしてコード修正を行いました。

github.com

まとめ

説明がとても長くなってしまいました。

今回の記事では、3つの製品に関する合計6つの脆弱性を紹介しましたが、まだ修正されていない脆弱性が合計15件あるため、それらに関する内容は今後修正がある程度完了した後に、改めてブログに投稿することにします。

長い文章を最後までご覧いただき、本当にありがとうございました。