59

我刚刚在阅读这篇文章《关于防止快速登录尝试 的基于表单的网站身份验证的权威指南》 。

最佳实践 #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 信息。

如果您使用会话信息,最终用户(攻击者)可以强行调用新会话,绕过您的策略,然后立即重新登录。会话实现起来非常简单,只需将用户的最后一次已知登录时间存储在会话变量中,将其与当前时间相匹配,并确保延迟足够长。

如果你使用cookies,攻击者可以简单地拒绝cookies,总而言之,这确实是不可行的。

如果您跟踪 IP 地址,则需要以某种方式存储来自 IP 地址的登录尝试,最好是在数据库中。当用户尝试登录时,只需更新您记录的 IP 列表。您应该以合理的时间间隔清除此表,转储一段时间内未激活的 IP 地址。陷阱(总是有陷阱)是一些用户最终可能会共享一个 IP 地址,并且在边界条件下,您的延迟可能会无意中影响用户。由于您正在跟踪失败的登录,并且只跟踪失败的登录,因此这不会造成太大的痛苦。

于 2010-01-19T03:47:35.537 回答
4

简短的回答是:不要这样做。您不会保护自己免受暴力破解,甚至可能使您的情况变得更糟。

所提出的解决方案都不起作用。如果您使用 IP 作为任何限制参数,攻击者将只会跨越大量 IP 进行攻击。如果您使用会话(cookie),攻击者只会丢弃任何 cookie。你能想到的总和是,绝对没有什么是暴力攻击者无法克服的。

但是,有一件事 - 您只依赖尝试登录的用户名。因此,不要查看所有其他参数,您可以跟踪用户尝试登录和限制的频率。但是攻击者想要伤害你。如果他认识到这一点,他也会暴力破解用户名。

这将导致几乎所有用户在尝试登录时都被限制到您的最大值。您的网站将毫无用处。攻击者:成功。

您通常可以将密码检查延迟大约 200 毫秒 - 网站用户几乎不会注意到这一点。但是一个蛮力的人会。(同样,他可以跨越 IP)但是,所有这些都不能保护您免受暴力破解或 DDoS 攻击——因为您不能以编程方式进行。

做到这一点的唯一方法是使用基础设施。

您应该使用 bcrypt 而不是 MD5 或 SHA-x 来散列您的密码,如果有人窃取您的数据库,这将使解密您的密码变得更加困难(因为我猜您在共享或托管主机上)

很抱歉让您失望了,但是这里的所有解决方案都有一个弱点,并且没有办法在后端逻辑中克服它们。

于 2012-06-30T03:55:51.980 回答
4

登录过程需要降低成功和不成功登录的速度。登录尝试本身的速度永远不应超过 1 秒。如果是,蛮力使用延迟来知道尝试失败,因为成功比失败短。然后,每秒可以评估更多组合。

每台机器的同时登录尝试次数需要受到负载均衡器的限制。最后,您只需要跟踪相同的用户或密码是否被多个用户/密码登录尝试重复使用。人类的打字速度不能超过每分钟 200 个字。因此,每分钟超过 200 个单词的连续或同时登录尝试来自一组机器。因此,这些可以安全地通过管道传输到黑名单,因为它不是您的客户。每台主机的黑名单时间不需要大于约 1 秒。这永远不会给人类带来不便,但无论是串行还是并行,都会对蛮力尝试造成严重破坏。

2 * 10^19 combinations at one combination per second, run in parallel on 4 billion separate IP addresses, will take 158 years to exhaust as a search space. To last one day per user against 4 billion attackers, you need a fully random alphanumeric password 9 places long at a minimum. Consider training users in pass phrases at least 13 places long, 1.7 * 10^20 combinations.

This delay, will motivate the attacker to steal your password hash file rather than brute force your site. Use approved, named, hashing techniques. Banning the entire population of Internet IP for one second, will limit the effect of parallel attacks without a dealy a human would appreciate. Finally, if your system allows more than 1000 failed logon attempts in one second without some response to ban systems, then your security plans have bigger problems to work on. Fix that automated response first of all.

于 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 回答
2

恕我直言,防御 DOS 攻击最好在 Web 服务器级别(甚至可能在网络硬件中)处理,而不是在您的 PHP 代码中。

于 2010-01-19T22:36:27.373 回答
1

我通常会创建登录历史记录和登录尝试表。尝试表将记录用户名、密码、IP 地址等。查询表以查看是否需要延迟。我建议在给定时间(例如一小时)内完全阻止超过 20 次的尝试。

于 2010-01-19T03:45:57.130 回答
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)。您还可以调整算法,以便根据先前的延迟和文件上的时间戳计算延迟。

高温高压

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);
}

如果攻击者同时向登录页面发送请求会发生什么?可能所有请求都会以相同的优先级运行,并且很可能在其他请求超过第二行之前没有请求到达 increment_attempt_number 指令。所以每个请求都会获得相同的 $time 和 $attempts 值并被执行。对于复杂的应用程序来说,防止这种安全问题可能很困难,并且涉及锁定和解锁数据库的某些表/行,当然会减慢应用程序的速度。

于 2010-01-21T00:42:42.040 回答
0

cballuo 提供了一个很好的答案。我只是想通过提供支持 mysqli 的更新版本来报答。我稍微改变了 sqls 中的表/字段列和其他小东西,但它应该可以帮助任何寻找 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 回答