3

私が開発したサイトは最近、ブルートフォース攻撃またはレインボーテーブル攻撃によって侵害されました。元のログインスクリプトにはSALTがなく、パスワードはMD5に保存されていました。

以下は、SALTとIPアドレスの禁止を含む更新されたスクリプトです。さらに、同じIPアドレスまたはアカウントで4回のログインに失敗した場合、Maydayの電子メールとSMSを送信し、アカウントを無効にします。よく見て、何が改善できるのか、何が欠けているのか、そして何がまったく奇妙なのかを教えてください。

<?php
    //Start session
    session_start();
    //Include DB config
    include $_SERVER['DOCUMENT_ROOT'] . '/includes/pdo_conn.inc.php';

    //Error message array
    $errmsg_arr = array();
    $errflag = false;

    //Function to sanitize values received from the form. Prevents SQL injection
    function clean($str) {
        $str = @trim($str);
        if(get_magic_quotes_gpc()) {
            $str = stripslashes($str);
        }
        return $str;
    }

    //Define a SALT, the one here is for demo
    define('SALT', '63Yf5QNA');

    //Sanitize the POST values
    $login = clean($_POST['login']);
    $password = clean($_POST['password']);
    //Encrypt password
    $encryptedPassword = md5(SALT . $password);
    //Input Validations
    //Obtain IP address and check for past failed attempts
    $ip_address = $_SERVER['REMOTE_ADDR'];
    $checkIPBan = $db->prepare("SELECT COUNT(*) FROM ip_ban WHERE ipAddr = ? OR login = ?");
    $checkIPBan->execute(array($ip_address, $login));
    $numAttempts = $checkIPBan->fetchColumn();
    //If there are 4 failed attempts, send back to login and temporarily ban IP address
    if ($numAttempts == 1) {
        $getTotalAttempts = $db->prepare("SELECT attempts FROM ip_ban WHERE ipAddr = ? OR login = ?");
        $getTotalAttempts->execute(array($ip_address, $login));
        $totalAttempts = $getTotalAttempts->fetch();
        $totalAttempts = $totalAttempts['attempts'];
        if ($totalAttempts >= 4) {
            //Send Mayday SMS
            $to = "admin@somewhere.com";
            $subject = "Banned Account - $login";
            $mailheaders = 'From: noreply@somewhere.com' . "\r\n";
            $mailheaders .= 'Reply-To: noreply@somewhere.com' . "\r\n";
            $mailheaders .= 'MIME-Version: 1.0' . "\r\n";
            $mailheaders .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
            $msg = "<p>IP Address - " . $ip_address . ", Username - " . $login . "</p>";
            mail($to, $subject, $msg, $mailheaders);
            $setAccountBan = $db->query("UPDATE ip_ban SET isBanned = 1 WHERE ipAddr = '$ip_address'");
            $setAccountBan->execute();
            $errmsg_arr[] = 'Too Many Login Attempts';
            $errflag = true;    
        }
    }
    if($login == '') {
        $errmsg_arr[] = 'Login ID missing';
        $errflag = true;
    }
    if($password == '') {
        $errmsg_arr[] = 'Password missing';
        $errflag = true;
    }

    //If there are input validations, redirect back to the login form
    if($errflag) {
        $_SESSION['ERRMSG_ARR'] = $errmsg_arr;
        session_write_close();
        header('Location: http://somewhere.com/login.php');
        exit();
    }

    //Query database
    $loginSQL = $db->prepare("SELECT password FROM user_control WHERE username = ?");
    $loginSQL->execute(array($login));
    $loginResult = $loginSQL->fetch();

    //Compare passwords
    if($loginResult['password'] == $encryptedPassword) {
        //Login Successful
        session_regenerate_id();
        //Collect details about user and assign session details
        $getMemDetails = $db->prepare("SELECT * FROM user_control WHERE username = ?");
        $getMemDetails->execute(array($login));
        $member = $getMemDetails->fetch();
        $_SESSION['SESS_MEMBER_ID'] = $member['user_id'];
        $_SESSION['SESS_USERNAME'] = $member['username'];
        $_SESSION['SESS_FIRST_NAME'] = $member['name_f'];
        $_SESSION['SESS_LAST_NAME'] = $member['name_l'];
        $_SESSION['SESS_STATUS'] = $member['status'];
        $_SESSION['SESS_LEVEL'] = $member['level'];
        //Get Last Login
        $_SESSION['SESS_LAST_LOGIN'] = $member['lastLogin'];
        //Set Last Login info
        $updateLog = $db->prepare("UPDATE user_control SET lastLogin = DATE_ADD(NOW(), INTERVAL 1 HOUR), ip_addr = ? WHERE user_id = ?");
        $updateLog->execute(array($ip_address, $member['user_id']));
        session_write_close();
        //If there are past failed log-in attempts, delete old entries
        if ($numAttempts > 0) {
            //Past failed log-ins from this IP address. Delete old entries
            $deleteIPBan = $db->prepare("DELETE FROM ip_ban WHERE ipAddr = ?");
            $deleteIPBan->execute(array($ip_address));
        }
        if ($member['level'] != "3" || $member['status'] == "Suspended") {
            header("location: http://somewhere.com");
        } else {
            header('Location: http://somewhere.com');
        }
        exit();
    } else {
        //Login failed. Add IP address and other details to ban table
        if ($numAttempts < 1) {
        //Add a new entry to IP Ban table
        $addBanEntry = $db->prepare("INSERT INTO ip_ban (ipAddr, login, attempts) VALUES (?,?,?)");
        $addBanEntry->execute(array($ip_address, $login, 1));
        } else {
            //increment Attempts count 
            $updateBanEntry = $db->prepare("UPDATE ip_ban SET ipAddr = ?, login = ?, attempts = attempts+1 WHERE ipAddr = ? OR login = ?");
            $updateBanEntry->execute(array($ip_address, $login, $ip_address, $login));
        }
        header('Location: http://somewhere.com/login.php');
        exit();
    }
?>

編集

さて、これがランダムソルトでの私の試みです。まず、テーブルに挿入するソルトを作成します。

define('SALT_LENGTH', 15);
function createSalt()
{
    $key = '!@#$%^&*()_+=-{}][;";/?<>.,';
    $salt = substr(hash('sha512',uniqid(rand(), true).$key.microtime()), 0, SALT_LENGTH);
    return $salt;
}
$salt = createSalt()
//More prep for entering into table...

次に、ランダムソルトを使用してハッシュを生成します。

$hash = hash('sha256', $salt . $pw); //$pw is the cleaned user submitted password

ユーザーがログインしたら、保存されたランダムに生成されたソルトを使用して、保存されたハッシュを比較します。

$loginHash = hash('sha256', $dbSalt . $pw);
if ($loginHash == $dbHash) {
    //Logged in
} else {
    //Failed
}

それはどのように見えますか?

4

5 に答える 5

8

わかりました、ここにいくつかあります:

  1. もう使用しないでmd5ください。できます、より良い方法があります(関数sha512で使用されるなどhash())。

  2. 私もはるかに長い静的塩を使用します。少なくとも64文字をお勧めします(結局のところ、書き込み時に計算するオーバーヘッドは最小限ですが、推測するのははるかに困難です)。

  3. また、動的(ランダム)ソルトも追加します。ユーザーごとに新しいものを生成し、パスワードハッシュと一緒に保存します(:文字で区切るのが一般的です)。このように、静的ソルトが危険にさらされた場合でも、データベース内のパスワードごとに新しいレインボーテーブルを生成(または少なくとも反復)する必要があります...

  4. 一時的でないものについては、IPアドレスに基づいて信頼または運用しないでください。ほとんどのISPは、単一のIPから複数のユーザーが表示されるNATの形式を使用します(これは、IPv4名前空間の枯渇によってのみ普及します)。IPアドレスをレート制限または一時的にブロックする場合は、問題ありません。しかし、それらを禁止しないでください...

  5. 関数clean()は、最初に文字列をチェックするか、文字列を強制的に文字列にする必要があります:($str = is_string($str) ? trim($str) : (string) $str;)。SQLインジェクションをまったく妨げることはありません。ただし、設定さstripslashesれたサーバーでコードを機能させるには(正確にどのように呼び出しているか)呼び出しが必要です(magic_quotes_gpc引用符をエスケープしようとします)...したがって、それを維持します。

  6. コードをより適切にフォーマットします。関連するタスクを処理する関数を作成します。そうすれば、何が起こっているのかを理解するために調べるための75行の手続き型コードがありません。さらに良いことに、それをクラスにラップし、一般的なタスク(dbアクセスなど)を独自のクラスに移動します。そして、適切にインデントすることを忘れないでください。読みやすさが重要なので、近道をとらないでください...

編集:パスワードを検証する方法に関しては、最初にソルトされたハッシュをフェッチしてから、保存されているソルトを使用してハッシュを再計算します。(以下に示す関数は、キーストレッチmakeSaltedHashと呼ばれるものを追加で使用します。

function validatePassword($password, $hash) {
    list($oldHash, $salt) = explode(':', $hash, 2);
    $newHash = makeSaltedHash($password, $salt);
    return $hash == $newHash;
}

function makeSaltedHash($password, $salt = '') {
    if (empty($salt)) {
        $salt = makeRandomSalt(mt_rand(64, 128));
    }
    $hash = hash('sha512', $password . $salt . SALT);
    for ($i = 0; $i < 50; $i++) {
        $hash = hash('sha512', $password . $salt . SALT . $hash);
    }
    return $hash . ':' . $salt;
}

function makeRandomSalt($length = 64) {
    $salt = '';
    for ($i = 0; $i < $lenght; $i++) {
        $salt .= chr(mt_rand(33, 126));
    }
    return $salt;
}
于 2011-01-03T14:08:36.523 に答える
4

私からの2つのヒント:

  • SQLインジェクションを防ぐために、ストリップスラッシュや魔法の引用符を使用しないでください。PDOパラメーターを使用します。例外なし。
  • ユーザーごとに異なるソルトを使用します。ソルトは秘密ではありません。ユーザーレコードと一緒にDBに保存してください。すべての人に同じソルトを使用すると、データベースがより攻撃されやすくなります。

無関係ですが、見過ごされがちです:ユーザーのパスワードの長さを制限しないでください。パスワードの長さに任意の制限(12文字など)を課すWebサイトが多すぎますが、複雑さのルールは笑えるものです(「少なくとも上、下、数字、特殊文字が1つありますが、「<」はありません。 '>' "またはそのようなナンセンス)。これは非常に敵対的です、避けてください。

于 2011-01-03T14:02:48.147 に答える
1
  1. 「安全」とは見なされなくなったMD5を使用しています。

  2. あなたは本当にもっと長い塩が必要です。

塩漬けを効果的に使用するには、この質問を参照してください。

于 2011-01-03T13:51:23.017 に答える
0

禁止すべきではありません-2回失敗した後にreCAPTCHAを挿入しますが、本当にmemcacheを使用してIPを保存したい場合。set($ ip、true、false、$ secondsTTL); 後でget($ ip)で確認します-TTLを2時間に設定します。また、すべてを関数に入れて、文字列引用符または二重引用符に何を使用したいかを調べたい場合もあります。;)

全体としては機能するように見えますが、読みにくく、冗長なものもあります。

于 2011-01-03T14:00:48.223 に答える
-5

clean()を追加する必要がありますmysql_real_escape_string()

サーバーやその他の設定によって$_SERVER['REMOTE_ADDR']は、空にすることができます。

exitリダイレクト後を忘れたときのエラーを防ぐために、簡単なリダイレクト関数を作成することをお勧めしますheader()

例:

function redirect($url) {
  if (!headers_sent()) {
     header('Location: '.$url);
  } else {
     // echo $url;
  }
  exit;
}
于 2011-01-03T13:50:36.883 に答える