19.01.2016 | 06:49

Adam Haertle

Pięć ciekawych przykładów rzadziej spotykanych błędów w aplikacjach WWW

Jako że Polacy także swoich łowców błędów mają, poprosiliśmy jednego z nich o podzielenie się z Wami ciekawymi przykładami błędów. Zapraszamy na spotkanie z fachowcem, który regularnie namierza i opisuje wpadki programistów.

Autorem tekstu jest Kacper Szurek zajmujący się znajdowaniem błędów w aplikacjach. Klikadziesiąt odkrytych w ostatnich miesiącach błędów opublikował już na swoim blogu i Twiterze. Zapraszamy do lektury opisów pięciu błędów wybranych na tę okazję przez Kacpra.

Omijanie filtrów

Załóżmy, że chcemy pozwolić użytkownikowi na wyświetlenie dowolnego pliku z katalogu ./photos/ oprócz secret.jpg:

$filename = basename((string) $_GET['file']);
if (stristr($filename, 'secret.jpg') === false) {
            echo file_get_contents('./photos/'.$filename);
}

Na pierwszy rzut oka wszystko wydaje się w porządku. Sprawdzamy bowiem czy w ciągu, który podał użytkownik nie znajduje się wyraz „secret.jpg”. Nie zadziałają zatem takie przykłady jak:

secret.jpg
../photos/secret.jpg

Jednakże sprawa wygląda nieco inaczej w przypadku PHP działającego na systemie Windows. W dokumencie Oddities of PHP file access in Windows®  możemy wyczytać iż w tym systemie ciąg << zamieniany jest na * a ponieważ gwiazdka oznacza symbol wieloznaczny (wild card) zostanie wyświetlony pierwszy plik pasujący do ciągu. Możemy zatem ominąć filtr używając:

sec<<

Oprócz * możemy także użyć:

” – zamienany na . (kropka), np. secret”jpg

> – zamieniany na ?, który oznacza dowolny znak, np. secret.jp>

Porównywanie ciągu znaków w PHP

W kolejnym przykładzie chcemy zabezpieczyć stronę przed nieautoryzowanym dostępem. W tym celu aby wyświetlić tajną treść musimy podać hasło, które następnie porównywane jest z tym zapisanym w kodzie.

Ze względów bezpieczeństwa nie przechowujemy hasła w czystej postaci lecz używamy algorytmu md5.

$password = (isset($_GET['password']) ? (string) $_GET['password'] : '');
if ('0e462097431906509019562988736854' == md5($password)) {
            echo 'Tajna treść';
}

Wiemy również, że prawidłowym hasłem jest „240610708”. Okazuje się jednak iż w podanym wyżej przykładzie działa także ciąg „QNKCDZO”. Na pierwszy rzut oka można pomyśleć iż jest to kolizja md5. Sprawdźmy zatem:

md5('240610708') = '0e462097431906509019562988736854'
md5('QNKCDZO') = '0e830400451993494058024219903391'

Może więc błąd w PHP? Zauważmy iż wynikowe ciągi znaków zaczynają się od „0e”. W dokumentacji czytamy: „The value is given by the initial portion of the string. If the string starts with valid numeric data, this will be the value used. Otherwise, the value will be 0 (zero). Valid numeric data is an optional sign, followed by one or more digits (optionally containing a decimal point), followed by an optional exponent. The exponent is an 'e’ or 'E’ followed by one or more digits.”.

Oznacza to iż jeśli porównamy dwa ciągi znaków zaczynające się od „0e” i zawierające same liczby, zostaną one zrzutowane na liczbę całkowitą. W podanej wyżej sytuacji dochodzi zatem do porównania:

0*10^462097431906509019562988736854 = 0*10^830400451993494058024219903391

Podane wyżej wartości pochodzą z Twittera @spazef0rze. Podobny atak został wykorzystany do resetowania hasła administratora w Simple Machines Forum. W celu ochrony przed tego typu atakami należy porównywać wartości przy użyciu specjalistycznych funkcji, np. hash_equals().

Length extension attack

Jesteśmy dużą firmą prowadzącą szkolenia, po których dostaje się imienne certyfikaty z wydrukowanym kodem certyfikatu. Kod ten można potem sprawdzić na naszej stronie w celu weryfikacji, czy dana osoba rzeczywiście przebyła nasz kurs.

$text = (isset($_GET['text']) ? (string) $_GET['text'] : '');
$hash = (isset($_GET['hash']) ? (string) $_GET['hash'] : '');
$secret = sha1('TO_JEST_MEGA_TAJNE_HASLO');

if ($hash === sha1($secret.$text)) {
      echo 'Certifkat dla: '.$text;
 } else {
            echo '<a href="certyfikat.php?text=test&hash='.sha1($secret.'test').'">Testowy certyfikat</a>';
}

Jak widać firma udostępnia również testowy certifkat w celu sprawdzenia funkcjonalności portalu. Nie zgłębiając się w szczegóły kryptografii okazuje się, iż jest możliwe wygenerowanie prawidłowego certyfikatu dla innej osoby bez znajomości tajnego hasła. Generalnie wszystkie kombinacje:

tajny_kod.dane_kontrolowane_przez_użytkownika

są podatne na atak length extension. Podczas demonstracji użyjemy narzędzia hash extender. Zakładając że dla danych „test” certyfikat to „cd43c014ff62d01131ad02fb8341976fe7161ab2” możemy użyć takie komendy:

./hash_extender --data=74657374 --data-format=hex --signature=cd43c014ff62d01131ad02fb8341976fe7161ab2 --format=sha1 --append=4B6163706572537A7572656B --append-format=hex --out-data-format=html --secret=40

–data = „test” zakodowany w HEX

–data-format = wybieramy tryb hex

–signature = przekopiowany kod certyfikatu

–format = format certyfikatu

–append = jakie dane chcemy dokleić, w tym przypadku KacperSzurek jako hex

–append-format = w jakim formacie są doklejane dane

–secret = długość tajnego kodu użytego do weryfikacji danych

Po wywołaniu programu otrzymujemy:

Type: sha1
Secret length: 40
New signature: 16eb4d555c848a6f3fe207c1e8e63684f6342d7e
New string: test%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%01%60KacperSzurek

Możemy zatem użyć linku:

certyfikat.php?text=test%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%01%60KacperSzurek&hash=16eb4d555c848a6f3fe207c1e8e63684f6342d7e

który prawidłowo wskaże: „Certifkat dla: test`KacperSzurek”. W celu ochrony przed tego typu atakami należy stosować HMAC.

XSS bez liter

Pewnego dnia dochodzimy do wniosku, że chcemy udostępnić użytkownikom naszego portalu prosty kalkulator obsługujący nawiasy. Aby sprawę maksymalnie uprościć posłużymy się JavaScript używając funkcji eval(), która wykonuje podany przez nas kod. Ponieważ zależy nam na bezpieczeństwie sprawdzamy czy użytkownik nie podaje czasem liter w celu ataku na nasz serwis.

$input = (isset($_POST['input']) ? (string) $_POST['input'] : '');
if (preg_match('/[a-zA-Z]/', $input)) {
            $output = "'Wrong'";
} else {
            $output = $input;
}

?>
<script>
            eval("alert(<?php echo $output; ?>)");
</script>
<form method="post">
            <input type="text" name="input">
            <input type="submit" value="Wylicz">
</form>

Kalkulator działa jak należy. Obsługuje operacje takie jak:

(4*2)+6

a nie pozwala na wprowadzenie jakiegokolwiek tekstu, na przykład:

document.cookie

Niestety okazuje się, iż możliwy jest atak na taki serwis. W tym celu skorzystamy z JSFuck. Język ten wykorzystuje jedynie 6 znaków: ()[]!+ aby zapisać dowolny kod. Ciąg „xss” wygląda zatem następująco:

(+(+!+[]+[+[]]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(+![]+([]+[])[([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(+![]+[![]]+([]+[])[([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]](!+[]+!+[]+!+[]+[!+[]+!+[]+!+[]+!+[]])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]

Istnieją różne alternatywy dla podanej strony, na przykład Hieroglyphy wykorzystujący 8 znaków.

Zaufanie do Google

Po ostatnim audycie szefostwo w firmie postanowiło wprowadzić na oficjalnej stronie internetowej politykę CSP w celach ochrony przed atakami XSS. Ponieważ na stronie używany jest CDN od Google postanowiono zezwolić na użycie domeny „ajax.googleapis.com”.

<?php
header('Content-Security-Policy: default-src \'self\' ajax.googleapis.com');
header("X-XSS-Protection: 0");
?>
<?php echo $_GET['xss']; ?>

Niestety podczas ostatniej aktualizacji systemu do kodu wkradł się drobny błąd pozwalający na XSS. Jego naprawa jednak nie otrzymała wysokiego priorytetu ponieważ i tak możliwy był jedynie w teorii ze względu na CSP. Okazuje się jednak, że ślepe ufanie Google w każdej sprawie nie zawsze przynosi korzyść. Na serwerach CDN znajduje się sporo różnych wersji biblioteki AngularJS. Jak czytamy na stronie poświęconej bezpieczeństwu tej biblioteki w wersji 1.0.8 oraz 1.1.5 występuje błąd pozwalający na ominięcie polityki CSP. Możemy zatem używając linku:

<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script><div ng-app ng-csp><h1 ng-mouseover="$event.target.ownerDocument.defaultView.alert('XSS')">Najedź na mnie</h1></div>

dokonać ataku. Najpierw dołączamy podatną wersje Angulara z serwerów Google. Ponieważ domena „ajax.googleapis.com” została dodana do polityki wszystko działa jak należy. Następnie tworzymy nowy obiekt div i informujemy bibliotekę iż korzystamy z CSP. Dalej tworzymy prosty payload z wykorzystaniem funkcji biblioteki. Inne kombinacje tego ataku możemy znaleźć na stronie zagadki XSS by mario. Podobny sposób został wykorzystany do ominięcia zabezpieczeń dodatku NoScript dla Firefoxa czy też ScriptBlock dla Chrome.

Powrót

Komentarze

  • 2016.01.19 08:05 Robert

    Może mi sie wydaje, ale tworzony soft jest i będzie coraz bardziej podatny na tego typu „ingerencje”. Wystarczy zobaczyć jak dużo stron urzędowych służy pozycjenerom niebieskich tabletek.

    Odpowiedz
    • 2016.01.19 10:42 Piotr

      Nie ma co się dziwić, skoro większość tych stron nie jest moderowana, a co dopiero poprawnie administrowana. Zauważ, że tam gdzie więcej kasy wpakowano w X stronę tym większy na niej bajzel.

      Odpowiedz
  • 2016.01.19 08:14 Michał

    Bardzo ciekawe przykłądy, dzięki.

    Dwie uwagi:

    1) W przykładzie „Porównywanie ciągu znaków w PHP” wydaje mi się, że problemem jest użycie == zamiast ===. Po zmianie działa jak trzeba: http://phpfiddle.org/main/code/37nx-rtqu.

    2) W przykładzie „XSS bez liter” użyty JSFuck korzysta z 6 a nie 4 znaków: ()[]+! (http://www.jsfuck.com/)

    Odpowiedz
    • 2016.01.19 14:45 Rafał

      Dokładnie tak! Operator równości może jest wygodny w niektórych sytuacjach, ale jest też nieprzewidywalny. Przy dokonywaniu porównywań czegokolwiek zawsze należy używać operatora identyczności.

      Odpowiedz
    • 2016.01.19 16:10 Kacper

      1) Oprócz === pomogło by również strcmp, chociaż i tam łatwo o popełnienie błędu: http://stackoverflow.com/a/14674029
      Podczas porównywania haseł użycie hash_equals() jest preferowane ponieważ tak jak wspomniał @red funkcja ta broni przed timing attack
      2) Tak, masz rację.

      Odpowiedz
  • 2016.01.19 08:24 red

    „W celu ochrony przed tego typu atakacji należy porównywać wartości przy użyciu specjalistycznych funkcji, np. hash_equals().”

    Albo po prostu użyć prawidłowego operatora porównania ===. hash_equals() broni przed timing attacks.

    Odpowiedz
    • 2016.01.19 14:45 Sebo

      Czekamy aż PHP udostępni „tym-razem-nam-chyba-wyszło” operator porównania „====” który przy okazji komparacji napisów sprawdzi też certyfikat ssl, oceni stan SMART dysku w serwerze i hash ramu kernela. Tak na wszelki wypadek.

      PHP: Lepsze jest wrogiem dziadowskiego.

      Odpowiedz
      • 2016.01.19 16:39 Sebastian

        Haha mistrz, już tylko w wersji 8.0!

        Odpowiedz
  • 2016.01.19 09:13 Wacław

    file_get_contents() robi globa, żeby znaleźć plik? Po co? I to tylko na Windows?

    Odpowiedz
  • 2016.01.19 09:17 dawg1

    Świetny tekst. Bardzo cenne przykłady!

    Odpowiedz
  • 2016.01.19 09:42 Vokiel

    Mario Heiderich robi bardzo dużo ciekawych rzeczy w ramach szeroko-pojętego bezpieczeństwa. Polecam śledzić jego poczynania.

    Odpowiedz
  • 2016.01.19 10:48 Janunsz Kamiński

    Bardzo, bardzo cenny artykuł!

    Odpowiedz
  • 2016.01.19 13:05 Jacek

    Nie znam PHP, ale nie powinno było być?

    0*10^462097431906509019562988736854 = 0*10^830400451993494058024219903391

    Też zero, ale inne :)

    Odpowiedz
    • 2016.01.19 16:14 Kacper

      Rzeczywiście wkradł się błąd.
      „e” w PHP oznacza zapis w notacji naukowej.

      Odpowiedz
  • 2016.01.19 13:12 Jacek

    JSFuck używa sześciu znaków: ()[]!+

    Odpowiedz
  • 2016.01.19 14:34 MatM

    Porady kompletnie bezużyteczne dla osób tworzących aplikacje webowe w technologii innej niż PHP. Może macie eksperta od Pythona, Javy czy Ruby? Ile można wałkować przykłady wynikające z błędnego podejścia do implementacji i wdrożenia? Jeżeli ktoś serwuje statyczne pliki z tego samego serwera co dynamiczne strony albo trzyma kod aplikacji w document root serwera to znaczy, że ma poważniejsze problemy niż tylko błędy w implementacji PHP i zbyt małą liczbę znaków „=” w if-ach. To tak jakbyście chcieli bronić się przed złodziejem już po wpuszczeniu go do domu/mieszkania – owszem można ale to raczej nie jest optymalne rozwiązanie.

    Odpowiedz
    • 2016.01.19 16:24 Kacper

      @MatM pierwsze dwa przykłady rzeczywiście przydadzą się osobą tworzącym strony w PHP. Kolejne trzy natomiast są uniwersalne.
      Kod z przykładu o length extension attack można przepisać do innego języka.
      Błędy XSS zdarzają się wszędzie a JSFuck można wykorzystać również w celu ominięcia filtrów, jeśli strona korzysta z jakiegoś Web Application Firewall.
      CSP również powinno być zaimplementowane na każdej stronie, bez względu na to, w jakiej technologii jest stworzona.

      Odpowiedz
  • 2016.01.19 16:59 zxx

    Same konkrety – swietny artykul. Gratulacje

    Odpowiedz
  • 2016.01.19 18:19 Łowca

    Pierwszy akapit: „Jako że Polacy nie gęsi i swoich łowców błędów mają(…)” Oświadczam, że mianuję się na takiego „niegęsiego” błędów łowcę. I już tłumaczę.

    Mikołaj Rej pisząc: „A niechaj narodowie wżdy postronni znają, iż Polacy nie gęsi, iż swój język mają”, użył słowa „gęsi” jako przymiotnika.

    Gdyby zamiast gęsi użyć konia, cytat brzmiałby: (…)iż Polacy nie koński, iż swój język mają. Podsumowując, nie chodzi o to czym Polacy nie są, ale jakim językiem powinni się posługiwać. Nie powinni pisać tylko po gęsiemu (łacinie), ale przede wszystkim po swojemu (po polsku).

    Odpowiedz
    • 2016.01.26 03:16 szczyglis

      Tego się żem tutaj nie spodziewał.
      You made my day :)

      Odpowiedz
  • 2016.03.18 12:16 king

    chociaż jak się popatrzy na tego jsfuck i pochodne to też dałoby się go odfiltrować. to kwestia walidacji danych.
    najważniejsze parametry to oczekiwany zakres długości i oczekiwany zestaw znaków.

    Odpowiedz

Zostaw odpowiedź do Kacper

Jeśli chcesz zwrócić uwagę na literówkę lub inny błąd techniczny, zapraszamy do formularza kontaktowego. Reagujemy równie szybko.

Pięć ciekawych przykładów rzadziej spotykanych błędów w aplikacjach WWW

Komentarze