Pergunta

eu estava lendo esse post O guia definitivo para autenticação de sites baseada em formulário sobre como evitar tentativas de login rápido.

Melhor prática nº 1:Um pequeno atraso que aumenta com o número de tentativas malsucedidas, como:

1 tentativa falhada = sem atraso
2 tentativas fracassadas = atraso de 2 segundos
3 tentativas fracassadas = atraso de 4 segundos
4 tentativas fracassadas = atraso de 8 segundos
5 tentativas fracassadas = atraso de 16 segundos
etc.

Um ataque DoS a este esquema seria muito impraticável, mas, por outro lado, potencialmente devastador, uma vez que o atraso aumenta exponencialmente.

Estou curioso para saber como poderia implementar algo assim para meu sistema de login em PHP?

Foi útil?

Solução

Você não pode simplesmente impedir os ataques do DOS, enchendo a limitação de um único nome de IP ou usuário. Inferno, você nem pode impedir as tentativas de login de fogo rápido usando esse método.

Por quê? Porque o ataque pode abranger várias IPs e contas de usuário em questão de ignorar suas tentativas de limitação.

Eu vi postados em outro lugar que, idealmente, você deve rastrear todas as tentativas de login fracassadas em todo o site e associá -las a um registro de data e hora, talvez:

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;

Uma nota rápida no campo IP_Address: você pode armazenar os dados e recuperar os dados, respectivamente, com INT_ATON () e INT_NTOA (), que essencialmente equivalem a converter um endereço IP de e para um número inteiro não assinado.

# 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;

Decidir sobre certos limiares de atraso com base no No geral Número de logins com falha em um determinado período de tempo (15 minutos neste exemplo). Você deve basear isso em dados estatísticos retirados do seu failed_logins Tabela como vai Muda com o tempo Com base no número de usuários e em quantos deles podem lembrar (e digitar) sua senha.


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

Consulte a tabela em cada tentativa de login fracassada de encontrar o número de logins com falha por um determinado período de tempo, digamos 15 minutos:


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

Se o número de tentativas durante o período em que o tempo estiver acima do seu limite, aplique a limitação ou force todos os usuários a usar um CAPTCHA (ou seja, recaptcha) até que o número de tentativas fracassadas durante o período de tempo seja menor que o limite.

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

O uso do Recaptcha em um determinado limite garantiria que um ataque de várias frentes fosse interrompido e os usuários normais do site não experimentem um atraso significativo para tentativas de login com falha legítima.

Outras dicas

Você tem três abordagens básicas:armazenar informações de sessão, armazenar informações de cookies ou armazenar informações de IP.

Se você usar informações de sessão, o usuário final (invasor) poderá invocar novas sessões à força, ignorar sua tática e fazer login novamente sem demora.As sessões são bastante simples de implementar, basta armazenar o último horário de login conhecido do usuário em uma variável de sessão, compará-lo com o horário atual e certificar-se de que o atraso foi longo o suficiente.

Se você usar cookies, o invasor pode simplesmente rejeitá-los; em suma, isso realmente não é algo viável.

Se você rastrear endereços IP, precisará armazenar tentativas de login de um endereço IP de alguma forma, de preferência em um banco de dados.Quando um usuário tentar fazer logon, basta atualizar sua lista registrada de IPs.Você deve limpar esta tabela em um intervalo razoável, descartando endereços IP que não estão ativos há algum tempo.A armadilha (sempre há uma armadilha) é que alguns usuários podem acabar compartilhando um endereço IP e, em condições de limite, seus atrasos podem afetar os usuários inadvertidamente.Como você está rastreando logins com falha, e apenas logins com falha, isso não deve causar muita dor.

O processo de login precisa reduzir sua velocidade para login bem -sucedido e malsucedido. A tentativa de login nunca deve ser mais rápida que cerca de 1 segundo. Se for, a força bruta usa o atraso para saber que a tentativa falhou porque o sucesso é mais curto que a falha. Em seguida, mais combinações podem ser avaliadas por segundo.

O número de tentativas simultâneas de login por máquina precisa ser limitado pelo balanceador de carga. Por fim, você só precisa rastrear se o mesmo usuário ou senha for reutilizado por mais de uma tentativa de login de usuário/senha. Os seres humanos não podem digitar mais rápido que cerca de 200 palavras por minite. Portanto, tentativas de login sucessivas ou simultâneas mais rapidamente que 200 palavras por minite são de um conjunto de máquinas. Portanto, eles podem ser canalizados para uma lista preta com segurança, pois não é seu cliente. Os tempos de lista pretos por host não precisam ser maiores que cerca de 1 segundo. Isso nunca irá incomodar um humano, mas causará estragos com uma tentativa de força bruta, seja em série ou paralelo.

2 * 10^19 Combinações em uma combinação por segundo, executadas em paralelo em 4 bilhões de endereços IP separados, levarão 158 anos para esgotar como um espaço de pesquisa. Para durar um dia por usuário contra 4 bilhões de invasores, você precisa de uma senha alfanumérica totalmente aleatória 9 lugares no mínimo. Considere o treinamento de usuários em frases de passagem pelo menos 13 lugares, 1,7 * 10^20 combinações.

Esse atraso motivará o invasor a roubar seu arquivo de hash de senha, em vez de forçar o seu site. Use técnicas de hash aprovadas, nomeadas. A proibição de toda a população de IP da Internet por um segundo limitará o efeito de ataques paralelos sem um humano, um humano apreciaria. Por fim, se o seu sistema permitir mais de 1000 tentativas de logon com falha em um segundo sem alguma resposta para proibir os sistemas, seus planos de segurança têm problemas maiores para trabalhar. Corrija essa resposta automatizada antes de tudo.

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.

ou como sugerido por Cyro:

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

É um pouco difícil, mas os componentes básicos estão lá. Se você atualizar esta página, cada vez que atualizará o atraso ficará mais longo.

Você também pode manter as contagens em um banco de dados, onde verifica o número de tentativas com falha pelo IP. Ao usá -lo com base no IP e manter os dados do seu lado, você evita que o usuário possa limpar seus cookies para interromper o atraso.

Basicamente, o código inicial seria:

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

Store Fail Tentativas no banco de dados por IP. (Como você tem um sistema de login, presumo que você saiba bem como fazer isso.)

Obviamente, o Sessions é um método tentador, mas alguém realmente dedicado pode facilmente perceber que pode simplesmente excluir seu cookie de sessão em tentativas fracassadas, a fim de contornar completamente o acelerador.

Na tentativa de fazer login, busque quantas tentativas de login recentes (digamos, nos últimos 15 minutos) houve e o tempo da última tentativa.

$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!";
}

Você pode usar sessões. Sempre que o usuário falha em um login, você aumenta o valor que armazena o número de tentativas. Você pode imaginar o atraso necessário do número de tentativas ou definir o tempo real que o usuário também pode tentar novamente na sessão.

Um método mais confiável seria armazenar as tentativas e o novo tempo no banco de dados para esse iPaddress específico.

IMHO, a defesa contra os ataques do DOS é melhor tratada no nível do servidor da Web (ou talvez até no hardware da rede), não no seu código PHP.

Eu geralmente crio histórias de login e tabelas de tentativa de login. A tabela de tentativas registraria nome de usuário, senha, endereço IP, etc. Consulta contra a tabela para ver se você precisa atrasar. Eu recomendaria o bloqueio completamente para tentativas superiores a 20 em um determinado tempo (uma hora, por exemplo).

De acordo com a discussão acima, sessões, cookies e endereços IP não são eficazes - todos podem ser manipulados pelo invasor.

Se você deseja evitar ataques de força bruta, a única solução prática é basear o número de tentativas no nome de usuário fornecido, no entanto, observe que isso permite que o invasor doe o site, impedindo que os usuários válidos fizessem login.

por exemplo

$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
 }

Observe que, conforme escrito, este procedimento vaza informações de segurança - ou seja, será possível que alguém que ataque o sistema veja quando um usuário efetua login (o tempo de resposta para os invasores tentará cair para 0). Você também pode ajustar o algoritmo para que o atraso seja calculado com base no atraso anterior e no registro de data e hora no arquivo.

Hth

C.

É claro que cookies ou métodos baseados em sessões são inúteis neste caso. O aplicativo deve verificar o endereço IP ou o registro de data e hora (ou ambos) das tentativas anteriores de login.

Uma verificação de IP pode ser ignorada se o invasor tiver mais de um IP para iniciar suas solicitações e puder ser problemático se vários usuários se conectarem ao seu servidor do mesmo IP. Neste último caso, alguém que não faz o login por várias vezes impediria que todos que compartilhassem o mesmo IP de login com esse nome de usuário por um determinado período de tempo.

Uma verificação de carimbo de data / hora tem o mesmo problema acima: todos podem impedir que todos os outros registrem uma conta específica apenas tentando várias vezes. Usar um captcha em vez de uma longa espera pela última tentativa é provavelmente uma boa solução alternativa.

As únicas coisas extras que o sistema de login deve impedir são as condições de corrida na tentativa de verificação da função. Por exemplo, no seguinte pseudocódigo

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

O que acontece se um atacante enviar simultâneo Solicitações para a página de login? Provavelmente, todas as solicitações serão executadas com a mesma prioridade, e é provável que nenhuma solicitação chegue à instrução increment_attemp_number antes que os outros ultrapassem a segunda linha. Portanto, cada solicitação recebe o mesmo valor $ tempo e $ tenta valor e é executado. A prevenção desse tipo de problema de segurança pode ser difícil para aplicativos complexos e envolve o bloqueio e o desbloqueio de algumas tabelas/linhas do banco de dados, é claro que diminui a desaceleração do aplicativo.

A resposta curta é: não faça isso. Você não se protege de forçar bruto, pode até piorar sua situação.

Nenhuma das soluções propostas funcionaria. Se você usar o IP como qualquer parâmetro para acelerar, o invasor abrange o ataque por um grande número de IPs. Se você usar a sessão (cookie), o invasor soltará biscoitos. A soma de tudo em que você consegue pensar é que não há absolutamente nada que um atacante forçando bruto não possa superar.

Porém, há uma coisa - você apenas confia no nome de usuário que tentou fazer login. Portanto, não olhando para todos os outros parâmetros que acompanha com que frequência um usuário tentou fazer login e acelerar. Mas um atacante quer prejudicá -lo. Se ele reconhecer isso, ele também apenas nomes de usuário brutos.

Isso resultará em quase todos os seus usuários sendo reduzidos ao seu valor máximo quando tentarem fazer login. Seu site será inútil. Atacante: sucesso.

Você pode atrasar a verificação de senha em geral por cerca de 200ms - o usuário do site quase não notará isso. Mas um forçador bruto. (Novamente, ele poderia abranger o IPS) No entanto, nada disso o protegerá de forçar bruto ou DDOs - como você não pode programar.

A única maneira de fazer isso é usar a infraestrutura.

Você deve usar o BCRYPT em vez de MD5 ou SHA-X para hash suas senhas, isso tornará muito mais difícil descriptografar suas senhas se alguém roubar seu banco de dados (porque acho que você está em um host compartilhado ou gerenciado)

Desculpe por decepcioná-lo, mas todas as soluções aqui têm uma fraqueza e não há como superá-las dentro da lógica de back-end.

Cballuo forneceu uma excelente resposta. Eu só queria devolver o favor, fornecendo uma versão atualizada que suporta MySQLI. Eu mudei um pouco as colunas de tabela/campo nos SQLs e outras pequenas coisas, mas isso deve ajudar qualquer pessoa que procure o equivalente do 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;
        }
     }        
  }
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top