232
255

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ホワイトハッカーへの道 二歩目

Last updated at Posted at 2019-02-11

ファイルアップロード機能の脆弱性を考える

Webサイトのセキュリティ対策でまず知っておきたいのがファイルアップロードに対する対策です。
ホワイトハッカーへの道 一歩目で見てきたローカルプロキシーツールを使えば、運用中のサイトについて検証が可能です。

ローカルプロキシーツールburpsuiteを使った攻撃手順は下の動画を参考にしてください。
https://www.youtube.com/watch?time_continue=134&v=LLF-DNsFCfU

攻撃手法

ファイルアップロードからどんな攻撃があるかを見ていきましょう。

BKDR_ZZPEG (JPGファイルのExifにスクリプトを埋め込む攻撃)

Exif とは「Exchangeable image file format」の略称です。
jpgにはデータサイズや画素数、圧縮の種類などの情報を保管できる領域が設けられています。
この領域にスクリプトを埋め込み他のファイルからこのjpgファイルのExif情報を実行する攻撃です。
手法はこちらを参考にしてください。

FileUploadAttack (ヘッダーのcontent-typeを改竄してスクリプトをアップロードする攻撃)

アップロード処理の穴をついてスクリプトファイルをサーバにアップロードし、アップロードしたスクリプトファイルを通して意図しない動作を引き起こす攻撃です。
手法はこちらを参考にしてください。

ファイル名にパス情報を追加してアップロードする攻撃

ファイル名に「../../hoge.jpg」などのパス情報を含めアップロードします。
アプリケーションで適切な対応を取らないとサーバに書き込み、実行権限のあるフォルダにWebshellをアップロードされ実行されてしまいます。

  • WebShellとはphpなどのスクリプトからshellコマンドが実行できてしまうスクリプトファイルのことです。

対策

アプリケーション、サーバに以下の対策がなされているか確認します。

  • ファイル名にパスが含めれないかチェックする。
  • ファイルにスクリプトタグが含まれていないかのチェックする。
  • MIMEタイプに対応する拡張子をチェックする。
  • アップロードサイズをチェックする。
  • アップロード時にEXIFは削除する。
  • アップロードされたファイル名をランダムなファイル名にする。
  • アップロードディレクトリ、ファイルは実行権限を付与しない。
  • アップロード先をドキュメントルート外フォルダかクラウドにする。

サンプルコード

PHPではグローバル変数$_FILESを使って、$_FILES[inputのname属性][アップロードファイルの項目]でファイル情報を取得できます。取得できる項目は以下になります。

項目 内容
name 元ファイル名
type ファイルタイプ
tmp_name サーバーに一時保管されたファイル名
error エラーコード
size ファイルサイズ

またerrorの値を使ってエラーチェックが可能です。

コード エラー定数 説明
0 UPLOAD_ERR_OK エラーなし
1 UPLOAD_ERR_INI_SIZE php.iniのupload_max_filesizeのサイズを超えている
2 UPLOAD_ERR_FORM_SIZE HTMLフォームで指定された MAX_FILE_SIZE を超えている
3 UPLOAD_ERR_PARTIAL 一部のみしかアップロードされていない
4 UPLOAD_ERR_NO_FILE アップロードされなかった
6 UPLOAD_ERR_NO_TMP_DIR サーバー一時保管フォルダがない
7 UPLOAD_ERR_CANT_WRITE ディスクへの書き込みに失敗
8 UPLOAD_ERR_EXTENSION PHPの拡張モジュールがアップロードを中止した

こちらを使ってファイルアップロード時のエラーチェックを作成します。

権限は実行権限のない644に変更してます。

d r w x
ディレクトリ 読み 書き 実行
記号 rw- r-- r--
属性値 6 4 4
(4+2+0) (4+0+0) (4+0+0)
自分 グループ 他人
<?php
$fileName = $_FILES['upload']['name'];
$tmpName  = $_FILES['upload']['tmp_name'];
$fileSize = $_FILES['upload']['size'];
$extentison = pathinfo($fileName, PATHINFO_EXTENSION);

try {
    // ファイル名にパスが含めれないかチェック
    if (basename(realpath($fileName)) !== $fileName) {
       throw new RuntimeException('ファイル名が不正です');
    }

    // ファイルにスクリプトタグが含まれていないかのチェック
    if (count(token_get_all($fileName)) >= 2)  {
       throw new RuntimeException('ファイル形式が不正です');
    }

    // MIMEタイプに対応する拡張子チェック
    if ($extentison != array_search(
        mime_content_type($tmpName),
        array('gif' => 'image/gif', 'jpg' => 'image/jpeg', 'png' => 'image/png',)
        , true)
     ) {
        throw new RuntimeException('ファイル形式が不正です');
     }

    // $_FILES['upload']['error']の値チェック
    // アップロードサイズをチェック
    if (!isset($_FILES['upload']['error']) 
    || !is_int($_FILES['upload']['error'])
    ) {
       throw new RuntimeException('ファイル形式が不正です');
    } else {
       switch ($_FILES['upload']['error']) {
            case UPLOAD_ERR_OK:
                break;
            case UPLOAD_ERR_NO_FILE:
                throw new RuntimeException('ファイルが選択されていません');
            case UPLOAD_ERR_INI_SIZE:
            case UPLOAD_ERR_FORM_SIZE:
                throw new RuntimeException('ファイルサイズが大きすぎます');
            default:
                throw new RuntimeException('その他のエラーが発生しました');
       }
    }

    // アップロード時にJPEGの場合はEXIFをGDで削除
    list(, , $type) = getimagesize($tmpName);
    if($type === IMG_JPG) {
        // 新規サイズを取得
        $gd = imagecreatefromjpeg($tmpName);
        $w = imagesx($gd);
        $h = imagesy($gd);
        // 再サンプル
        $out = imagecreatetruecolor($w, $h);
        imagecopyresampled($out, $gd, 0, 0, 0, 0, $w, $h, $w, $h);
        // 元画像を削除した上で同一名でファイルを再生成
        unlink($tmpName);
        imagejpeg($out, $tmpName, 100);
        imagedestroy($gd);
    }

    // アップロードされたファイル名をランダムなファイル名にする。
    // ドキュメントルート外のフォルダを指定する クラウドストレージが安全。
    if (!move_uploaded_file(
            $tmpName, 
            $path = sprintf('./uploads/%s.%s', sha1_file($tmpName),
            $extentison
        )
    )) {
        throw new RuntimeException('ファイル保存時にエラーが発生しました');
    }

    // アップロードファイルは実行権限のない状態へ
    chmod($path, 0644);

    echo '正常アップロード';
} catch (RuntimeException $e) {
    echo $e->getMessage();
}

こちらのコードは検証中です。
そのまま使うのは自己責任でお願いいたします。

まとめ

ファイルアップロードには危険がいっぱいです。フレームワークを入れても上記の攻撃は基礎として対応することが必要です。攻撃手法を理解し、対策を実施しましょう。
また他の攻撃手法やそれに対する対策がある方はアドバイス頂けますでしょうか。

三歩目は「今、改めてXSSを考える」です。

2019/02/19 更新

ファイルアップロードでアップロードされたWebShellについて
理解を深めるには下記の記事が大変参考になります。
https://qiita.com/WhatRune/items/f89581fb6b4041d7c68a

232
255
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
232
255

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?