(Nie)bezpieczny kod – CSRF

CSRF jest metodą ataku umożliwiającą na wykonanie akcji przeznaczonej dla zalogowanego użytkownika bez konieczności logowania się do systemu przez atakującego.

W artykule wykorzystuję kod napisany na potrzeby ataku XSS, a więc do pełnego zrozumienia niezbędne jest przeczytanie poprzedniego wpisu z serii (Nie)bezpieczny kod.

Trochę teorii

Cross-site request forgery (w skrócie CSRF lub XSRF) ma na celu skłonić zalogowanego użytkownika serwisu internetowego do tego, aby uruchomił on odnośnik, którego otwarcie wykona akcję, do której atakujący nie miałby dostępu. Podsyłając spreparowany odnośnik zalogowanemu administratorowi można na przykład „za jego pośrednictwem” usunąć informacje z bazy danych.

Przykładowy atak CSRF

Żeby umożliwić atak potrzebujemy nieco zmodyfikować plik logowanie.php:

<?php
$dbh = new PDO('mysql:host=****;dbname=****', '****', '****');
session_start();
if (!isset($_SESSION['logged'])) {
    $_SESSION['logged'] = false;
}
if (isset($_GET['action'])) {
    if ($_GET['action'] == 'logout') {
        $_SESSION['logged'] = false;
        session_destroy();
    }
}
if ($_SESSION['logged'] === false && isset($_POST['login']) && isset($_POST['password'])) {
    if ($_POST['login'] == 'demo' && $_POST['password'] == 'demo'
    ) {
        $_SESSION['logged'] = true;
    } else {
        echo '<p>Złe hasło!!!</p>';
        $_SESSION['logged'] = false;
    }
}
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
    <meta charset="utf-8">
    <title>CSRF - logowanie</title>
</head>
<body>
<?php if ($_SESSION['logged']):
    if (isset($_GET['action'])) {
        if ($_GET['action'] == 'delete' && isset($_GET['id'])) {
            $posts = $dbh->prepare("DELETE from post WHERE id=" . (int)$_GET['id']);
            $posts->execute();
        }
    }
    ?>
    Jesteś zalogowany! <a href="?action=logout">Wyloguj mnie!</a>
<?php else: ?>
    <form method="post" action="">
        Login: <input type="text" name="login"/><br/>
        Hasło: <input type="password" name="password"/><br/><br/>
        <input type="submit" value="Zaloguj"/>
    </form>
<?php endif; ?>
</body>
</html>

W powyższym kodzie dodałem możliwość usuwania postu dla zalogowanego administratora.

Pora teraz na właściwą część ataku, a więc dodanie spreparowanego obrazka do wpisu :).

Przykładowy atak CSRF

W miejsce adresu do obrazka wpisuję url wykonujący akcję przeznaczoną tylko dla zalogowanego użytkownika – w tym wypadku usuwam post o ID równyym 1. Po dodaniu kodu wystarczy teraz tylko poczekać aż zalogowany administrator otworzy stronę…

Jak się zabezpieczyć przed CSRF?

Jednym z kanałów wszczepienia złośliwego kodu jest luka zezwalająca na XSS, a więc warto w pierwszej kolejności zadbać o zabezpieczenie się przed XSS.

Dodatkowym zabezpieczeniem jest zapisanie unikalnego tokena w sesji i doklejanie go do każdego linka prowadzącego do akcji przeznaczonych dla zalogowanych użytkowników. Przed wykonaniem akcji konieczne jest sprawdzenie, czy token z tablicy GET jest taki sam, jak zapisany w sesji.

Poprawiony plik logowanie.php wygląda następująco:

<?php
$dbh = new PDO('mysql:host=****;dbname=****', '****', '****');
session_start();
if (!isset($_SESSION['logged'])) {
    $_SESSION['logged'] = false;
}
if (isset($_GET['action'])) {
    if ($_GET['action'] == 'logout') {
        $_SESSION['logged'] = false;
        session_destroy();
    }
}
if ($_SESSION['logged'] === false && isset($_POST['login']) && isset($_POST['password'])) {
    if ($_POST['login'] == 'demo' && $_POST['password'] == 'demo'
    ) {
        $_SESSION['logged'] = true;
        $_SESSION['token'] = uniqid();
    } else {
        echo '<p>Złe hasło!!!</p>';
        $_SESSION['logged'] = false;
    }
}
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
    <meta charset="utf-8">
    <title>CSRF - logowanie</title>
</head>
<body>
<?php if ($_SESSION['logged']):
    if (isset($_GET['action'])) {
        if ($_GET['action'] == 'delete' && isset($_GET['id'])  && isset($_GET['token']) && $_GET['token'] == $_SESSION['token']) {
            $posts = $dbh->prepare("DELETE from post WHERE id=" . (int)$_GET['id']);
            $posts->execute();
        }
    }
    ?>
    Jesteś zalogowany! <a href="?action=logout">Wyloguj mnie!</a>
    <h4>Posty</h4>
    <ul>
        <?php
        $posts=$dbh->prepare("SELECT * from post ORDER by id DESC");
        $posts->execute();
        foreach($posts->fetchAll() as $post) {
            echo '<li><a href="?action-=delete&id='.$post['id'].'&token='.$_SESSION['token'].'">Usuń wpis '.$post['id'].'</a></li>';
        }
        ?>
    </ul>
<?php else: ?>
    <form method="post" action="">
        Login: <input type="text" name="login"/><br/>
        Hasło: <input type="password" name="password"/><br/><br/>
        <input type="submit" value="Zaloguj"/>
    </form>
<?php endif; ?>
</body>
</html>

W moim przykładzie link do usuwania wpisów wygląda mniej więcej tak:

logowanie.php?action-=delete&id=2&token=54ca4c85098f9

Poza tym bezpieczniej jest przekazywać dane metodą POST. Manipulacja nimi będzie znacznie trudniejsza.

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