59

私はちょうどこの投稿を読んでいたところですフォームベースのWebサイト認証の決定的なガイド高速ログイン試行の防止.

ベスト プラクティス #1: 次のように、失敗した試行の数に応じて増加する短い時間の遅延:

試行失敗 1 回 = 遅延なし 試行
失敗 2 回 = 遅延 2 秒 試行
失敗 3 回 = 遅延 4 秒 試行
失敗 4 回 = 遅延 8 秒 試行
失敗 5 回 = 遅延 16 秒
など。

このスキームを攻撃する DoS は非常に現実的ではありませんが、一方で、遅延が指数関数的に増加するため、壊滅的な被害をもたらす可能性があります。

PHP でログイン システムにこのようなものを実装するにはどうすればよいでしょうか?

4

12 に答える 12

83

スロットルを単一のIPまたはユーザー名に連鎖させることによってDoS攻撃を単純に防ぐことはできません。この方法を使用して、迅速なログイン試行を実際に防ぐことさえできません。

なんで? スロットルの試みを回避するために、攻撃は複数のIPとユーザーアカウントにまたがる可能性があるためです。

他の場所に投稿されているのを見たことがありますが、理想的には、サイト全体で失敗したすべてのログイン試行を追跡し、それらをタイムスタンプに関連付ける必要があります。

CREATE TABLE failed_logins (
    id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(16) NOT NULL,
    ip_address INT(11) UNSIGNED NOT NULL,
    attempted DATETIME NOT NULL,
    INDEX `attempted_idx` (`attempted`)
) engine=InnoDB charset=UTF8;

ip_addressフィールドに関する簡単な注意:INET_ATON()およびINET_NTOA()を使用して、データをそれぞれ保存および取得できます。これは、IPアドレスを符号なし整数に変換することと本質的に同じです。

# example of insertion
INSERT INTO failed_logins SET username = 'example', ip_address = INET_ATON('192.168.0.1'), attempted = CURRENT_TIMESTAMP;
# example of selection
SELECT id, username, INET_NTOA(ip_address) AS ip_address, attempted;

特定の時間(この例では15分)で失敗したログインの総数に基づいて、特定の遅延しきい値を決定します。これは、ユーザーの数とパスワードを思い出す(および入力する)ことができるユーザーの数に基づいて時間の経過とともに変化するfailed_loginsため、テーブルから取得した統計データに基づいて行う必要があります。


> 10 failed attempts = 1 second
> 20 failed attempts = 2 seconds
> 30 failed attempts = reCaptcha

ログインに失敗するたびにテーブルをクエリして、特定の期間、たとえば15分間に失敗したログインの数を見つけます。


SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute);

特定の期間の試行回数が制限を超えている場合は、スロットリングを強制するか、特定の期間の失敗した試行回数がしきい値を下回るまで、すべてのユーザーにキャプチャ(reCaptcha)の使用を強制します。

// array of throttling
$throttle = array(10 => 1, 20 => 2, 30 => 'recaptcha');

// retrieve the latest failed login attempts
$sql = 'SELECT MAX(attempted) AS attempted FROM failed_logins';
$result = mysql_query($sql);
if (mysql_affected_rows($result) > 0) {
    $row = mysql_fetch_assoc($result);

    $latest_attempt = (int) date('U', strtotime($row['attempted']));

    // get the number of failed attempts
    $sql = 'SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute)';
    $result = mysql_query($sql);
    if (mysql_affected_rows($result) > 0) {
        // get the returned row
        $row = mysql_fetch_assoc($result);
        $failed_attempts = (int) $row['failed'];

        // assume the number of failed attempts was stored in $failed_attempts
        krsort($throttle);
        foreach ($throttle as $attempts => $delay) {
            if ($failed_attempts > $attempts) {
                // we need to throttle based on delay
                if (is_numeric($delay)) {
                    $remaining_delay = time() - $latest_attempt - $delay;
                    // output remaining delay
                    echo 'You must wait ' . $remaining_delay . ' seconds before your next login attempt';
                } else {
                    // code to display recaptcha on login form goes here
                }
                break;
            }
        }        
    }
}

特定のしきい値でreCaptchaを使用すると、複数のフロントからの攻撃が阻止され、通常のサイトユーザーが正当なログイン試行の失敗によって大幅な遅延が発生することはありません。

于 2010-01-19T12:16:52.010 に答える
6

セッション情報の保存、Cookie 情報の保存、または IP 情報の保存の 3 つの基本的なアプローチがあります。

セッション情報を使用すると、エンド ユーザー (攻撃者) が新しいセッションを強制的に呼び出し、戦術をバイパスして、遅滞なく再度ログインする可能性があります。セッションの実装は非常に簡単です。ユーザーの最後の既知のログイン時刻をセッション変数に格納し、それを現在の時刻と照合し、遅延が十分に長いことを確認するだけです。

Cookie を使用すると、攻撃者は単に Cookie を拒否できますが、これは実際には実行可能なものではありません。

IP アドレスを追跡する場合は、IP アドレスからのログイン試行を何らかの方法で、できればデータベースに保存する必要があります。ユーザーがログオンを試みたら、記録された IP のリストを更新するだけです。このテーブルを妥当な間隔で消去して、しばらくアクティブになっていない IP アドレスをダンプする必要があります。落とし穴 (常に落とし穴があります) は、一部のユーザーが IP アドレスを共有することになる可能性があり、境界条件では、遅延がユーザーに不注意に影響を与える可能性があることです。失敗したログインを追跡していて、失敗したログインのみを追跡しているので、これはあまり苦痛を引き起こすべきではありません.

于 2010-01-19T03:47:35.537 に答える
4

簡単な答えは次のとおりです。これを行わないでください。総当たり攻撃から身を守ることはできません。状況を悪化させる可能性さえあります。

提案されたソリューションはどれも機能しません。スロットルのパラメータとしてIPを使用すると、攻撃者は膨大な数のIPに攻撃を仕掛けるだけです。セッション(Cookie)を使用する場合、攻撃者はCookieをドロップするだけです。あなたが考えることができるすべての合計は、ブルートフォース攻撃者が克服できなかったものは絶対にないということです。

ただし、1つだけあります。ログインを試みたユーザー名に依存するだけです。したがって、他のすべてのパラメーターを確認せずに、ユーザーがログインしてスロットルを試みた頻度を追跡します。しかし、攻撃者はあなたに危害を加えようとしています。彼がこれを認識した場合、彼はユーザー名も総当たり攻撃します。

これにより、ほとんどすべてのユーザーがログインしようとしたときに最大値に制限されます。Webサイトは役に立たなくなります。攻撃者:成功。

一般に、パスワードチェックを約200ミリ秒遅らせることができます。これは、Webサイトのユーザーがほとんど気付かないことです。しかし、ブルートフォーサーはそうします。(ここでも、彼はIPにまたがることができます)ただし、プログラムで実行できないため、ブルートフォースやDDoSから保護することはできません。

これを行う唯一の方法は、インフラストラクチャを使用することです。

パスワードをハッシュするには、MD5またはSHA-xの代わりにbcryptを使用する必要があります。これにより、誰かがデータベースを盗んだ場合にパスワードの復号化が非常に困難になります(共有または管理されたホスト上にいると思われるため)

失望させて申し訳ありませんが、ここにあるすべてのソリューションには弱点があり、バックエンドロジック内でそれらを克服する方法はありません。

于 2012-06-30T03:55:51.980 に答える
4

ログインプロセスでは、ログインが成功した場合と失敗した場合の両方で速度を落とす必要があります。ログインの試行自体は、約1秒より速くなることはありません。そうである場合、ブルートフォースは遅延を使用して、成功は失敗よりも短いため、試行が失敗したことを認識します。次に、1秒あたりにより多くの組み合わせを評価できます。

マシンごとの同時ログイン試行回数は、ロードバランサーによって制限される必要があります。最後に、同じユーザーまたはパスワードが複数のユーザー/パスワードのログイン試行によって再利用されているかどうかを追跡する必要があります。人間は1ミニットあたり約200語より速く入力することはできません。したがって、1ミニットあたり200ワードを超える連続または同時ログイン試行は、一連のマシンからのものです。したがって、これらは顧客ではないため、安全にブラックリストにパイプすることができます。ホストあたりのブラックリスト時間は、約1秒より長くする必要はありません。これは人間に不便をかけることは決してありませんが、シリアルまたはパラレルのいずれであっても、ブルートフォースの試みで大混乱を引き起こします。

1秒あたり1つの組み合わせで2*10 ^ 19の組み合わせ、40億の個別のIPアドレスで並行して実行される場合、検索スペースとして使い果たされるまでに158年かかります。40億人の攻撃者に対して、ユーザーごとに1日持続するには、完全にランダムな英数字のパスワードが少なくとも9桁の長さである必要があります。少なくとも13桁の長さ、1.7 * 10^20の組み合わせのパスフレーズでユーザーをトレーニングすることを検討してください。

この遅延により、攻撃者はサイトをブルートフォースするのではなく、パスワードハッシュファイルを盗むようになります。承認された名前付きのハッシュ手法を使用します。インターネットIPの全人口を1秒間禁止すると、人間が喜ぶような取引をせずに、並列攻撃の影響を制限できます。最後に、システムが禁止システムへの応答なしに1秒間に1000回を超える失敗したログオン試行を許可する場合、セキュリティ計画には取り組むべきより大きな問題があります。まず、その自動応答を修正します。

于 2012-12-03T23:49:45.507 に答える
3
session_start();
$_SESSION['hit'] += 1; // Only Increase on Failed Attempts
$delays = array(1=>0, 2=>2, 3=>4, 4=>8, 5=>16); // Array of # of Attempts => Secs

sleep($delays[$_SESSION['hit']]); // Sleep for that Duration.

またはCyroによって提案されたように:

sleep(2 ^ (intval($_SESSION['hit']) - 1));

少し大雑把ですが、基本的なコンポーネントはそこにあります。このページを更新すると、更新するたびに遅延が長くなります。

カウントをデータベースに保持して、失敗した試行の数を IP ごとに確認することもできます。IPに基づいて使用し、データをあなたの側に保持することにより、ユーザーがCookieをクリアして遅延を止めることができなくなります.

基本的に、最初のコードは次のようになります。

$count = get_attempts(); // Get the Number of Attempts

sleep(2 ^ (intval($count) - 1));

function get_attempts()
{
    $result = mysql_query("SELECT FROM TABLE WHERE IP=\"".$_SERVER['REMOTE_ADDR']."\"");
    if(mysql_num_rows($result) > 0)
    {
        $array = mysql_fetch_assoc($array);
        return $array['Hits'];
    }
    else
    {
        return 0;
    }
}
于 2010-01-19T03:44:48.657 に答える
3

失敗試行を IP ごとにデータベースに保存します。(ログイン システムがあるので、その方法をよく知っていると思います。)

明らかに、セッションは魅力的な方法ですが、本当に献身的な人であれば、スロットルを完全に回避するために、試行が失敗したときにセッション Cookie を削除するだけでよいことにすぐに気付くことができます。

ログインを試みると、最近 (過去 15 分間など) にログインが試行された回数と、最後に試行された時刻が取得されます。

$failed_attempts = 3; // for example
$latest_attempt = 1263874972; // again, for example
$delay_in_seconds = pow(2, $failed_attempts); // that's 2 to the $failed_attempts power
$remaining_delay = time() - $latest_attempt - $delay_in_seconds;
if($remaining_delay > 0) {
    echo "Wait $remaining_delay more seconds, silly!";
}
于 2010-01-19T03:46:45.097 に答える
2

セッションを使用できます。ユーザーがログインに失敗するたびに、試行回数を格納する値を増やします。試行回数から必要な遅延を計算するか、ユーザーがセッションで再試行できる実際の時間を設定することもできます。

より信頼性の高い方法は、その特定の IP アドレスのデータベースに試行と新しい試行時間を格納することです。

于 2010-01-19T03:45:19.467 に答える
1

上記の説明のとおり、セッション、Cookie、およびIPアドレスは効果的ではなく、攻撃者がすべてを操作する可能性があります。

ブルートフォース攻撃を防ぎたい場合、唯一の実用的な解決策は、提供されたユーザー名に基づいて試行回数を決定することです。ただし、これにより、攻撃者は有効なユーザーのログインをブロックすることでサイトをDOSできることに注意してください。

例えば

$valid=check_auth($_POST['USERNAME'],$_POST['PASSWD']);
$delay=get_delay($_POST['USERNAME'],$valid);

if (!$valid) {
   header("Location: login.php");
   exit;
}
...
function get_delay($username,$authenticated)
{
    $loginfile=SOME_BASE_DIR . md5($username);
    if (@filemtime($loginfile)<time()-8600) {
       // last login was never or over a day ago
       return 0;
    }
    $attempts=(integer)file_get_contents($loginfile);
    $delay=$attempts ? pow(2,$attempts) : 0;
    $next_value=$authenticated ? 0 : $attempts + 1;
    file_put_contents($loginfile, $next_value);
    sleep($delay); // NB this is done regardless if passwd valid
    // you might want to put in your own garbage collection here
 }

書かれているように、この手順はセキュリティ情報を漏らしていることに注意してください。つまり、システムを攻撃している誰かがユーザーがいつログインしたかを確認できる可能性があります(攻撃者の試みの応答時間は0に低下します)。以前の遅延とファイルのタイムスタンプに基づいて遅延が計算されるようにアルゴリズムを調整することもできます。

HTH

C。

于 2010-01-19T11:51:24.083 に答える
1

もちろん、この場合、Cookieやセッションベースのメソッドは役に立ちません。アプリケーションは、以前のログイン試行のIPアドレスまたはタイムスタンプ(あるいはその両方)をチェックする必要があります。

攻撃者がリクエストを開始するIPが複数ある場合はIPチェックをバイパスでき、複数のユーザーが同じIPからサーバーに接続する場合は問題が発生する可能性があります。後者の場合、誰かが何度もログインに失敗すると、同じIPを共有するすべてのユーザーがそのユーザー名で一定期間ログインできなくなります。

タイムスタンプチェックには、上記と同じ問題があります。誰もが、複数回試行するだけで、他のすべての人が特定のアカウントにログインするのを防ぐことができます。最後の試行を長時間待つ代わりにキャプチャを使用することは、おそらく良い回避策です。

ログインシステムが防止する必要がある唯一の追加事項は、試行チェック機能の競合状態です。たとえば、次の擬似コードでは

$time = get_latest_attempt_timestamp($username);
$attempts = get_latest_attempt_number($username);

if (is_valid_request($time, $attempts)) {
    do_login($username, $password);
} else {
    increment_attempt_number($username);
    display_error($attempts);
}

攻撃者がログインページに同時にリクエストを送信するとどうなりますか?おそらく、すべてのリクエストが同じ優先度で実行され、他のリクエストが2行目を超える前に、increment_attempt_number命令にリクエストが到達しない可能性があります。したがって、すべてのリクエストは同じ$timeと$attemptsの値を取得し、実行されます。この種のセキュリティ問題を防ぐことは、複雑なアプリケーションでは困難な場合があり、データベースの一部のテーブル/行をロックおよびロック解除する必要があります。もちろん、アプリケーションの速度は低下します。

于 2010-01-21T00:42:42.040 に答える
1

通常、ログイン履歴とログイン試行テーブルを作成します。試行テーブルには、ユーザー名、パスワード、IP アドレスなどが記録されます。テーブルに対してクエリを実行して、遅延する必要があるかどうかを確認します。特定の時間 (たとえば 1 時間) に 20 回を超える試行を完全にブロックすることをお勧めします。

于 2010-01-19T03:45:57.130 に答える
0

cballuo は優れた回答を提供しました。mysqli をサポートする更新バージョンを提供することで恩返しをしたかっただけです。SQLのテーブル/フィールドの列やその他の小さなものを少し変更しましたが、mysqliに相当するものを探している人には役立つはずです。

function get_multiple_rows($result) {
  $rows = array();
  while($row = $result->fetch_assoc()) {
    $rows[] = $row;
  }
  return $rows;
}

$throttle = array(10 => 1, 20 => 2, 30 => 5);

$query = "SELECT MAX(time) AS attempted FROM failed_logins";    

if ($result = $mysqli->query($query)) {

    $rows = get_multiple_rows($result);

$result->free();

$latest_attempt = (int) date('U', strtotime($rows[0]['attempted'])); 

$query = "SELECT COUNT(1) AS failed FROM failed_logins WHERE time > DATE_SUB(NOW(), 
INTERVAL 15 minute)";   

if ($result = $mysqli->query($query)) {

$rows = get_multiple_rows($result);

$result->free();

    $failed_attempts = (int) $rows[0]['failed'];

    krsort($throttle);
    foreach ($throttle as $attempts => $delay) {
        if ($failed_attempts > $attempts) {
                echo $failed_attempts;
                $remaining_delay = (time() - $latest_attempt) - $delay;

                if ($remaining_delay < 0) {
                echo 'You must wait ' . abs($remaining_delay) . ' seconds before your next login attempt';
                }                

            break;
        }
     }        
  }
}
于 2012-10-05T01:36:46.073 に答える