よくある攻撃の結果としてファイルの改ざんが挙げられます。プログラムを動かしいているマシンの中のファイルを何らかの手段で変更して、処理を壊したり悪いことをしたりするプログラムに書き換えるわけです。このファイル改ざんのパターンの一つとしてwebページの内容となるコードを書き換えて悪意のあるサイトにするパターンがあります。こうなるとユーザー側で対処するのは難しいです。正当な手順を使って正当なサイトを利用しているのに、そのサイトの中は異常になっているわけです。ユーザーがサイトを信用して新たに個人情報やクレジットカードなどの情報を入力するとそれが流出しだします。このようなファイル改ざんの対策の一つとして追加、削除、変更されたファイルを検知する方法があります。検知した結果、異常な変更だった場合はサイトの復元ないし停止をして被害を防止します。サーバーの中をどこでも好き勝手される様な場合では心もとない仕組みですが、webサイトの脆弱性などでは特定の範囲のファイルしか変更できないので、そういった際には役立ちます(そうそう変更できない場所に検知スクリプトを置いておけば、検知を無効化されにくくなります)。このファイル改ざん検知をPHPで簡易に行う例を紹介します。
実際のコードが次です。
<?php
/**
* ファイル改ざん検知スクリプト
* このスクリプトはcronなどで定期的に実行することを想定しています。
*/
// 設定の定義
// 同じディレクトリにあるこのファイルの名前と同じ名前で拡張子がconf.phpのファイルを設定ファイルとして読み込みます。
if(file_exists(pathinfo(__FILE__, PATHINFO_FILENAME) . '.conf.php')) {
$config = require pathinfo(__FILE__, PATHINFO_FILENAME) . '.conf.php';
} else {
// 設定ファイルが存在しない場合はデフォルトの設定を使用します。
$config = [
'directories' => [ // 監視対象のディレクトリのリスト
__DIR__ . DIRECTORY_SEPARATOR . '*',
],
'slackWebhookUrl' => null, // SlackのWebhook URL
'resultFilePath' => pathinfo(__FILE__, PATHINFO_FILENAME) . '.result.json', // 結果ファイルのパス
];
}
// 同じディレクトリにあるこのファイルの名前と同じ名前で拡張子がresult.jsonのファイルを結果ファイルとします。
// 結果ファイルが存在しない場合は作成されます。
$config['resultFilePath'] ??= pathinfo(__FILE__, PATHINFO_FILENAME) . '.result.json';
/**
* ディレクトリを再帰的にスキャンしてファイルのハッシュを取得します。
*
* @param string $directory 対象ディレクトリのパス
* @return array ファイルパスをキー、ハッシュを値とする連想配列
*/
function scanDirectory(string $directory): array
{
$fileHashes = [];
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
$directory, FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS
));
foreach($iterator as $file) {
/** @var SplFileInfo $file */
if($file->isFile()) {
// 絶対パスだと長すぎになりやすいのでこのファイルのディレクトリ配下のファイルについては相対パスで保存
$filePath = str_replace(__DIR__ . DIRECTORY_SEPARATOR, '', $file->getRealPath());
$hash = hash_file('sha256', $file->getRealPath());
$fileHashes[$filePath] = $hash;
}
}
return $fileHashes;
}
/**
* 現在のハッシュと前回のハッシュを比較して変更点を検出します。
*
* @param array $currentHashes 現在のハッシュの連想配列
* @param array $previousHashes 前回のハッシュの連想配列
* @return array 変更が検出されたファイルのリスト
*/
function compareHashes(array $currentHashes, array $previousHashes): array
{
$changes = ['added' => [], 'modified' => [], 'deleted' => []];
foreach($currentHashes as $path => $hash) {
if(!isset($previousHashes[$path])) {
$changes['added'][] = $path;
} elseif($previousHashes[$path] !== $hash) {
$changes['modified'][] = $path;
}
}
foreach(array_keys($previousHashes) as $path) {
if(!isset($currentHashes[$path])) {
$changes['deleted'][] = $path;
}
}
return $changes;
}
/**
* 変更を通知します。SlackのWebhook URLが提供されている場合はSlackに通知します。
* コンソールには必ず出力します。
*
* @param array $changes 変更点のリスト
* @param string|null $slackWebhookUrl SlackのWebhook URL
*/
function notifyChanges(array $changes, string|null $slackWebhookUrl = null): void
{
$message = "ファイル改ざん検知結果:\n";
if(empty(array_filter($changes))) {
$message .= "変更はありません。\n";
} else {
foreach($changes as $type => $files) {
if(!empty($files)) {
$message .= strtoupper($type) . ': ' . implode(', ', $files) . "\n";
}
}
}
if(isset($slackWebhookUrl)) {
$payload = json_encode(['text' => $message]);
$ch = curl_init($slackWebhookUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$result = curl_exec($ch);
if($result === false) {
// cURLエラーがあった場合、エラーメッセージをコンソールに出力
echo 'cURLエラー: ' . curl_error($ch) . "\n";
}
curl_close($ch);
}
echo $message;
}
/**
* メイン処理
* @param array $config
* @return void
*/
function main(array $config): void
{
// ディレクトリをスキャンしてハッシュを取得
$currentHashes = [];
foreach($config['directories'] as $directory) {
foreach(glob($directory, GLOB_ONLYDIR) as $d) {
$currentHashes += scanDirectory($d);
}
}
// 前回のハッシュを取得
$previousHashes = file_exists($config['resultFilePath']) ? json_decode(file_get_contents($config['resultFilePath']), true) : [];
// 変更を検出
$changes = compareHashes($currentHashes, $previousHashes);
// 変更を通知
if(!$previousHashes) {
notifyChanges([], $config['slackWebhookUrl']); // 初期状態を記録。$changesをそのまま渡すととんでもない量の通知が行われてしまう
} else {
notifyChanges($changes, $config['slackWebhookUrl']);
}
// 現在のハッシュを結果ファイルに保存
file_put_contents($config['resultFilePath'], json_encode($currentHashes, JSON_PRETTY_PRINT));
}
main($config);
このスクリプトは指定されたディレクトリの中を再帰的に探索して各ファイルのSHA-256ハッシュを計算し、変更を追跡することでファイルの改ざんを検知し、検知結果をログファイルに記録しSlackやコンソールに通知します。
このスクリプトの設定例です。`$config`配列には、監視対象のディレクトリ、SlackのWebhook URL(オプション)、結果を保存するJSONファイルのパスが含まれています。SlackのWebhook URLを設定することで、検出された変更をSlackに通知することができます。
<?php
// 設定ファイル例
return [
// 調査対象のディレクトリを列挙。glob形式もOK
'directories' => [
__DIR__.'/app',
__DIR__.'/bootstrap',
__DIR__.'/config',
__DIR__.'/database',
__DIR__.'/public',
__DIR__.'/resources',
__DIR__.'/routes',
__DIR__.'/storage/frameworks/*',
__DIR__.'/vendor',
],
'slackWebhookUrl' => null, // SlackのWebhook URLを設定
'resultFilePath' => 'pre_result.json', // 結果ファイルのパス
];
注意点としてこのスクリプトはwebサイト経由でアクセスできない場所に置く必要があります。というのもこのスクリプト自体が改ざんされたら検知が無意味になるためです。少なくともwebサイト上の脆弱性では改ざんが困難な領域に配置すべきです。それでも改ざんされる想定のマシンの中に置いてあるという点で完璧からずっと遠いです。
こういったファイル改ざん検知スクリプトを定期的に実行することで、不正なファイルの変更に長期間好き勝手されることを防げます。この記事では簡易なPHPスクリプトを紹介しましたが、ファイルの整合性を担保するセキュリティの仕組みは昔からあります。よりリッチで堅牢かつ柔軟なソフトなども調査すると出てきます。必要に応じて適切な防御方法を設けた方がいいです。