我开发的网站最近受到了损害,最有可能是暴力破解或彩虹桌攻击。原始登录脚本没有SALT,密码存储在MD5中。
以下是更新的脚本,包含禁止访问SALT和IP地址。此外,如果相同的IP地址或帐户尝试登录失败,它将发送五月天电子邮件和短信并禁用该帐户。请仔细看看,让我知道可以改进什么,缺少什么,以及什么是奇怪的。
<?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
}
看起来如何?
答案 0 :(得分:8)
好的,这里有几个:
您不应再使用md5
了。您可以,但有更好的方法(例如sha512
与hash()
功能一起使用)。
我也会使用更长的静态盐。我建议至少64个字符(毕竟,写入时计算的开销很小,但更难以猜测)。
我还会添加动态(随机)盐。为每个用户生成一个新用户,并将其与密码哈希一起存储(通常用:
字符分隔它们)。这样,即使您的静态盐被泄露,也需要为数据库中的每个密码生成一个新的彩虹表(或者至少迭代一次)...
不要基于IP地址信任或操作非暂时性的任何内容。大多数ISP使用一种形式的NAT,其中从一个IP中可以看到多个用户(并且这将随着IPv4名称空间的耗尽而变得更加普遍)。如果你想限速或临时阻止IP地址,那很好。但是不要禁止他们......
您的clean()
函数应首先检查字符串,或强制它为字符串:($str = is_string($str) ? trim($str) : (string) $str;
)。它根本不会阻止sql注入。但是stripslashes
调用是必要的(确切地说是如何拥有它)以允许代码在magic_quotes_gpc
设置的服务器上工作(它将尝试为你转义引号)...所以请保持这种状态
更好地格式化代码。创建处理相关任务的函数。这样你就没有75行程序代码来查看发生了什么。更好的是,将它包装在一个类中,并将常见任务(db访问等)移动到它们自己的类中。并记住正确缩进。可读性是王道,所以不要走捷径......
编辑:至于如何验证密码,首先获取盐渍哈希,然后使用存储的盐重新计算哈希值。 (我在下面显示的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;
}
答案 1 :(得分:4)
我的两个提示:
无关,但经常被忽视:不要限制用户的密码长度。我看过太多网站对密码长度施加了任意限制(如12个字符),但后来有可笑的复杂性规则(“至少有一个上部,一个下部,一个数字和一个特殊字符,但不是' &lt;','&gt;'“或这样的废话)。这是非常敌对的,避免它。
答案 2 :(得分:1)
您使用的是MD5,不再被视为“安全”。
你真的需要更长的盐。
为了有效使用腌制,请see this question。
答案 3 :(得分:0)
你不应该禁止 - 只是在2次尝试失败后再进行重新计算,但是如果你真的想用memcache来存储ip。 set($ ip,true,false,$ secondsTTL);稍后用get($ ip)检查 - 将TTL设置为2小时。 您可能还想将所有内容放入函数中,并找出您想要用于字符串引用或双引号的内容。 ;)
所有这些看起来都会起作用,但它真的难以阅读而且有些东西是多余的。
答案 4 :(得分:-5)
在clean()
中,您应该添加mysql_real_escape_string()
。
根据您的服务器和其他设置,$_SERVER['REMOTE_ADDR']
可能为空。
我建议创建一个简单的重定向功能,以防止在exit
重定向后忘记header()
时出错。
示例:
function redirect($url) {
if (!headers_sent()) {
header('Location: '.$url);
} else {
// echo $url;
}
exit;
}