Dość typowym sposobem realizacji funkcji resetu zapomnianego hasła jest generowanie losowego tokenu, który następnie wysyłany jest na zapisany w systemie adres e-mail. Korzystając z tego tokenu, można ustawić nowe hasło. O tym, że sposób ten nie jest najlepszy można przeczytać tutaj: OWASP Forgot Password Cheat Sheet i posłuchać tu: OWASP Podcast #83. Ale ja dzisiaj o trochę czym innym.
Co z tego, że Secure?
Jeśli sposób resetu hasła opiera się wyłącznie na losowym tokenie, to odgadnięcie tego tokenu daje możliwość zmiany hasła. A teraz zagadka, co jest nie tak z tym fragmentem kodu:
(...) SecureRandom r = new SecureRandom(); r.setSeed(System.currentTimeMillis()); r.nextBytes(buff); (...)
Posłużę się Jythonem:
>>> i = java.lang.System.currentTimeMillis() >>> s = java.security.SecureRandom() >>> s.setSeed(i) >>> s.nextInt() -2061508703 >>> s.nextInt() 421566032 >>> s.nextInt() -1127454972 >>>
I drugi fragment:
>>> r = java.security.SecureRandom() >>> r.setSeed(i) >>> r.nextInt() -2061508703 >>> r.nextInt() 421566032
Co jest nie tak? To, że te generatory zwracają taki sam ciąg wartości, a to dlatego, że zostały zainicjowane tym samym seedem. Nie jest to tajemnica, można stosowną informację znaleźć w dokumentacji:
The returned SecureRandom object has not been seeded. To seed the returned object, call the setSeed method. If setSeed is not called, the first call to nextBytes will force the SecureRandom object to seed itself. This self-seeding will not occur if setSeed was previously called.
Niestety, dalej jest gorzej:
public void setSeed(long seed)
Reseeds this random object, using the eight bytes contained in the given long seed. The given seed supplements, rather than replaces, the existing seed. Thus, repeated calls are guaranteed never to reduce randomness.
Coś tu się nie zgadza? Tak, jedna drobna rzecz - przypadek, kiedy funkcja setSeed jest wołana przed pierwszym "użyciem" generatora. Skutki widoczne na załączonym (wyżej obrazku) - generowany jest powtarzalny ciąg wartości.
A jakie skutki ma to w przypadku resetowania hasła? Wystarczy:
- ustalić czas na serwerze,
- wywołać funkcję resetowania hasła dla ofiary,
- sprawdzić możliwe wartości zwracane przez generator,
Ilość dostępnych prób jest często nieograniczona, jedynym ograniczeniem jest czas, przez który token jest ważny. W praktyce jednak nie jest to żadne utrudnienie dla atakującego, a więc dysponuje on metodą zmiany hasła dowolnego użytkownika.
Najdziwniejszym wykorzystaniem tego specyficznego zachowania SecureRandom, które widziałem, było (celowo powtarzalne) generowanie kluczy kryptograficznych w oparciu o wyjście SecureRandom inicjowane przez PIN użytkownika.
W całej historii jest tylko jedna pułapka - na różnych platformach zwracane wartości mogą różnić się między sobą. To znaczy ciąg generowany przez SecureRandom zainicjowany w ten sam sposób na Android i, przykładowo, na Windows, będzie (prawdopodobnie) różny.
Wniosek? Słowo Secure w nazwie nie zwalnia od myślenia...
I w temacie: Random number generation: An illustrated primer.
Chociaż imo korzystanie z rand()- podobnych funkcji do czegokolwiek związanego z authem to głupota, to się nadaje tylko to zabaw typu "zrób coś co x% ale w mniej-więcej losowych odstępach", zwłaszcza że /dev/urandom lub odpowiednik jest dostępne na wielu platformach
Inna sprawa to że zasada "seeduj pseuforand tylko raz" jest często olewane bez żadnego sensownego powodu
Date: Sat, 25 Feb 2012 13:12:58 GMT
Oczywiście w praktyce nie musi to być czas z maszyny, na której wykonuje się logika biznesowa, ale zwykle daje całkiem dobre przybliżenie.
Nie ma żadnego obrazka.
SecureRandom r = new java.security.SecureRandom();
System.out.println(r.getAlgorithm() + " / " + r.getProvider());
r.setSeed(100000000l);
System.out.println("1a: " + r.nextInt());
System.out.println("1b: " + r.nextInt());
r = new java.security.SecureRandom();
r.setSeed(100000000l);
System.out.println("2a: " + r.nextInt());
System.out.println("2b: " + r.nextInt());
zwraca za każdym razem różne wartości.
algotithm/provider: NativePRNG / SUN version 1.6
>>> r = java.security.SecureRandom()
>>> r.getAlgorithm()
u'SHA1PRNG'
>>> r.getProvider()
SUN version 1.6
>>> r.setSeed(100000000l)
>>> r.nextInt()
366127508
>>> r.nextInt()
203444209
>>> r = java.security.SecureRandom()
>>> r.setSeed(100000000l)
>>> r.nextInt()
366127508
>>> r.nextInt()
203444209
Między kolejnymi próbami rezultat dwóch pierwszych nextInt() różni się między sobą?
1a: 591922881
1b: 905758986
2a: 1185905089
2b: 521370691
Domyślam się, że test nie był na Windows?
Widać, że trzeba do własnego softu dodawać jeszcze akumulatory entropii
https://www.owasp.org/index.php/Forgot_Password_Cheat_Sheet
Posłuchaj tego podcastu:
https://www.owasp.org/download/jmanico/owasp_podcast_83.mp3
A potem zastanów się jak bardzo jest istotna Twoja aplikacja, jak bardzo ktoś może chcieć przejąć konto innego użytkownika. Zabezpieczenia powinny być w jakiś sposób adekwatne do istotności aplikacji. Nie powinny być ani za małe, ani za duże. Być może losowy token przesyłany na maila będzie wystarczający, mimo wszystkich swoich słabości.