3

我开发的一个网站最近遭到入侵,很可能是受到暴力破解或彩虹表攻击。原始登录脚本没有 SALT,密码存储在 MD5 中。

下面是一个更新的脚本,包含 SALT 和 IP 地址禁止。此外,如果相同的 IP 地址或帐户尝试 4 次登录失败,它将发送五月天电子邮件和 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();
    }
?>

编辑

好的,这是我对随机 Salt 的尝试。首先,创建要插入到表中的盐:

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了。可以但有更好的方法(如sha512hash()函数一起使用)。

  2. 我也会使用更长的静态盐。我建议至少 64 个字符(毕竟,在写入时计算开销很小,但更难猜)。

  3. 我还会添加动态(随机)盐。为每个用户生成一个新的,并将其与密码哈希一起存储(通常用:字符分隔它们)。这样,即使您的静态盐被破坏,也需要为您的数据库中的每个密码生成(或至少迭代)一个新的彩虹表......

  4. 不要信任或基于 IP 地址进行任何非临时性的操作。大多数 ISP 使用一种 NAT 形式,从一个 IP 可以看到多个用户(这只会随着 IPv4 命名空间的耗尽而变得更加普遍)。如果你想限制或暂时阻止 IP 地址,没问题。但不要禁止他们...

  5. 您的clean()函数应该首先检查字符串,或者强制它为字符串:( $str = is_string($str) ? trim($str) : (string) $str;)。它根本不阻止sql注入。但是stripslashes调用是必要的(确切地说是你如何拥有它)以允许代码在已magic_quotes_gpc设置的服务器上工作(这将尝试为你转义引号)......所以保持它。

  6. 更好地格式化您的代码。创建处理相关任务的函数。这样一来,您就无需查看 75 行程序代码来弄清楚发生了什么。更好的是,将其包装在一个类中并将常见任务(数据库访问等)移动到它们自己的类中。并记得正确缩进。可读性为王,所以不要走捷径...

编辑:至于如何验证密码,您首先获取加盐哈希,然后使用存储的盐重新计算哈希。(makeSaltedHash我在下面展示的函数额外使用了一种叫做Key Stretching的东西。

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

我的两个提示:

  • 不要使用斜杠或魔术引号来防止 SQL 注入。使用 PDO 参数。没有例外。
  • 为每个用户使用不同的盐。盐不是秘密,将其与用户记录一起存储在数据库中。对每个人使用相同的盐会使您的数据库更容易受到攻击。

无关,但经常被忽视:不要限制用户的密码长度。我见过太多的网站对密码长度施加了任意限制(比如 12 个字符),但是却有可笑的复杂性规则(“至少一个上限,一个下限,一个数字和一个特殊字符,但不是 '<', '>'”之类的废话)。这是非常敌对的,避免它。

于 2011-01-03T14:02:48.147 回答
1
  1. 您正在使用 MD5,它不再被认为是“安全的”。

  2. 你真的需要更长的盐。

为了有效地使用盐渍,请看这个问题

于 2011-01-03T13:51:23.017 回答
0

你不应该禁止 - 只是在 2 次失败尝试后输入一个 recaptcha,但如果你真的想使用 memcache 来存储 ip。设置($ip,真,假,$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 回答