Podczas ostatniej dyskusji na temat haseł maskowanych pojawiła się sugestia, że jeśli atakujący “nie widzi”, które znaki są wpisywane, wówczas odgadnięcie pełnego hasła jedynie na podstawie przechwyconych fragmentów haseł, jest trudne. No, w każdym razie trudniejsze. Temat już wówczas mnie zaintrygował, ale Q4 zbliżał się nieubłaganie i temat zarzuciłem. Pora wrócić do tego tematu.
Tytuł tego wpisu jest celowo mylący, wcale nie mam zamiaru debatować. Chcę natomiast odnieść się do tego komentarza. Adam napisał:
Nie rozumiem ludzi którzy korzystają z takich bajerów jak zapamiętywanie haseł za pośrednictwem programów czy serwerów, kiedy wystarczy sobie napisać własną funkcję i miksować 1 hasło przez url + md5, base64 itd, a na koniec np ucinać do 10 znaków i w 2ga stronę nie jest to możliwe do odtworzenia w praktyce.
Różnica między KeePass a opisanym przez Adama podejście jest taka, że hasło generowane przez KeePass jest losowe (właściwie: może być losowe, jeśli użytkownik sobie takie wygeneruje), natomiast hasło uzyskiwane w wyniku opisanej metody jest “wyprowadzane” na podstawie kilku danych wejściowych. Jeśli ktoś będzie w stanie odgadnąć “hasło główne” oraz ustalić sposób “wyliczania” hasła “docelowego”, będzie w stanie uzyskać hasło dla dowolnej strony. Mamy tutaj security through obscurity (tajny sposób “wyliczania” hasła) oraz swoisty class break. Co z tego wynika? Nic.
Wydaje mi się, że na temat trzeba spojrzeć nieco z szerszej perspektywy. A przy okazji – moim zdaniem głębokie ukrycie nie do końca zasługuje na oddzielny wpis w Wikipedii. Na dobrą sprawę jest to pewna forma security through obscurity, która w dodatku, w pewnych przypadkach i do pewnego czasu, działa.
Pokazanie przykładów niewłaściwego encodingu mamy za sobą (patrz: #1, #2, #3, #4 i #5). Na koniec przykład: http://bootcamp.threats.pl/lesson09b/, w którym encoding jest realizowany za pośrednictwem ESAPI (konkretnie owasp-esapi-php, jest to jeszcze wersja nieprodukcyjna). Dane wpisane przez użytkownika wypisywane są w trzech miejscach, w których encodowane są przy pomocy funkcji:
Funkcja doStuff obecnie coś robi, konkretnie wypisuje wartość otrzymanego parametru w drugim textarea. Zrobiłem to po to, by każdy mógł się przekonać, że te dziwne znaczki, które generuje ESAPI to rzeczywiście to, co zostało oryginalnie przekazane. A te dziwne znaczki wyglądają tak:
Dla encodeForJavaScript w kontekście atrybutu HTML:
demo 1
Dla encodeForJavaScript i encodeForHTMLAttribute w kontekście atrybutu HTML:
demo 2
Jeszcze raz dla encodeForJavaScript , tym razem w kontekście skryptu:
Mam nadzieję, że ten przykład wystarczająco dobrze pokazuje, że:
encoding może być prosty, wystarczy wywołać odpowiednią dla kontekstu funkcję,
encoding może być skuteczny (ktoś potrafi obejść encoding implementowany przez ESAPI i wykorzystany w tym przykładzie?),
mimo encodingu wszystko może nadal działać,
I tym optymistycznym akcentem kończę ten temat. Przynajmniej na jakiś czas.
Oryginał tego wpisu dostępny jest pod adresem Encoding: ESAPI
Jedyna różnica pojawia się przy znaku /, który w drugim przypadku zyskuje postać \/. Czy ma to jakieś znaczenie w tym konkretnym kontekście? Może w pewnym, niewielkim stopniu ma, ale przed XSS nie chroni, przykład:
“><foo bar=”
Różnica w stosunku do poprzedniego przykładu sprowadza się do tego, że w tym przypadku nie jestem w stanie uzyskać tagu:
ponieważ funkcja escapowania znaków spowoduje jego przekształcenie do postaci:
<\/script>
Użycie String.fromCharCode natomiast obchodzi drobną niedogodność związaną z brakiem znaków ', “ oraz / dostępnych w “czystej” postaci. Jak widać całkiem dobrze można sobie poradzić bez nich.
Raz, dwa, trzy (to teraz). To make long story short:
?@[\]^_`{|}~')“>demo 3
Co jest tu nie tak? Cóż, przykładowy payload, czyli:
'+alert(/xss/)+'
w tym przypadku nie zadziała, będzie rezultat w kodzie strony będzie wyglądał tak:
demo 3
Jak widać znak ' jest wypisywany jako \', co jest właściwym sposobem escapingu tego znaku. Podobnie zresztą jak \\\\” jest właściwym sposobem escapingu znaku “, uwaga, w JavaScript. Problem tylko w tym, że nie jesteśmy (wyłącznie) w kontekście JavaScript, ale również w kontekście atrybutu HTML. A skoro tak, to:
“><foo bar=”
da w rezultacie:
demo 3
Zagmatwane? Przeglądarka zobaczy to tak (IE i Firefox):
Poprzednio okazało się, że encoding HTML w kontekście atrybutu HTML, który zawiera JavaScript, nie jest zbyt skuteczny. Tak samo encoding właściwy dla JavaScript stosowany w przypadku JavaScript, który zawarty jest w atrybucie HTML, nie jest wystarczający. Zupełnie inaczej sytuacja wyglądałaby, gdyby złożyć ze sobą te dwa sposoby kodowania znaków. Najpierw dane wstawiane do kontekstu JavaScript zabezpieczyć w sposób właściwy dla JavaScript właśnie, a potem, skoro trafiają do atrybutu HTML, całość zakodować w sposób właściwy dla tego kontekstu. Wyglądałoby to mniej więcej tak:
demo 3
Jak zobaczy to przeglądarka – do sprawdzenia we własnym zakresie :)
Jak łatwo zauważyć, jedyną różnicą w stosunku do poprzedniego przypadku jest dodatkowe kodowanie znaku “. Dobrze to widać, jeśli oba przypadki zestawi się ze sobą:
demo 1
demo 2
Można się domyślić, że problemu z XSS w tym kontekście ta zmiana raczej nie rozwiązuje.
Pierwsze wyjaśnienie odnośnie ćwiczeń z nieprawidłowego encodingu. Na pierwszy ogień pójdzie pierwsze miejsce, w którym wprowadzone dane są wypisywane. Jako string testowy wykorzystam następujący zestaw znaków:
!“#$%25&'()*+,–./:;<=>?@[]^_`{|}~
Na wyjściu natomiast dostaje się coś takiego:
demo 1
Jak widać, specjalnie traktowane są tylko nieliczne znaki, które są przekształcane w następujący sposób:
& –> &
< –> <
> –> >
Byłoby to zabezpieczenie wystarczające, gdyby dane te były wypisywane w kontekście HTML. Problem w tym, że nie są. Są wypisywane w kontekście atrybutu HTML, który, tak się dodatkowo składa, zawiera kod JavaScript.