Chroniąc SSH przed zbieraniem adresów z known_hosts
Napisał: Patryk Krawaczyński
25/05/2019 w Administracja, Bezpieczeństwo Brak komentarzy. (artykuł nr 694, ilość słów: 1294)
J
eśli używasz SSH to Twój klient przechowuje w katalogu domowym listę mapującą nazwy hostów i adresy IP każdego zdalnego hosta, z którym się połączyłeś. Ta “baza danych”, znana jako plik known_hosts
może zostać wykorzystana przez atakujących, którzy naruszyli konta użytkowników. Rezultatem odczytania tego pliku jest “obraz” sieci, ujawniający, do których systemów mamy jeszcze połączenie. Może ułatwić to szkodliwemu oprogramowaniu i innym szkodliwym skryptom w rozprzestrzenianiu się na inne systemy, gdy tylko jeden system w sieci został skompromitowany. Plik ten jest dostępny w katalogu ~/.ssh każdego użytkownika, który chociaż raz łączył się jako klient SSH z zdalnym systemem. Jest on na tyle użyteczny, że w przypadku zmiany podpisu serwera – klient SSH będzie chronić użytkownika, powiadamiając go o tej sytuacji komunikatem typu:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdropping on you right now (man-in-the-middle attack)! It is also possible that the RSA host key has just been changed. The fingerprint for the RSA key sent by the remote host is 7c:9c:15:55:a5:dc:12:10:3f:dc:1f:a5:92:dc:e5:2c. Please contact your system administrator. Add correct host key in /home/user/.ssh/known_hosts to get rid of this message. Offending key in /home/user/.ssh/known_hosts:1 RSA host key for stardust.nfsec.pl has changed and you have requested strict checking. Host key verification failed.
W celu zmniejszenia ryzyka przechowywania tych danych w postaci czystego tekstu zostało wprowadzone rozwiązanie haszujące adresy serwerów w pliku known_hosts
(od wersji 4.0 OpenSSH). W celu włączenia tej funkcji należy opcję HashKnownHosts ustawić na wartość: yes. Opcję tę możemy znaleźć w pliku konfiguracyjnym klienta SSH odnoszącego się do całego systemu, którym zwykle jest /etc/ssh/ssh_config lub umieścić ją lokalnie tylko dla swojego konta w pliku ~/.ssh/config. Przed ustawieniem tej opcji wpisy będą miały postać:
stardust.nfsec.pl ssh-rsa AAAA...
Ostateczny wynik haszowania wpisów będzie wyglądał mniej więcej tak:
|1|XV5CFMH8LLIQPq7PxdBhGX7I9PA=|VKNLdODsQlJ/j4cvTZncqs9vgh0= ecdsa-sha2-nistp256 AAA...J
Zahaszowana nazwa hosta jest nieczytelna dla ludzkiego oka lub złośliwych skryptów. Dla każdego nowego połączenia z już powiązanym hostem algorytm haszowania utworzy ten sam łańcuch znaków. W ten sposób klient wie, że ma już przechowywany klucz i porówna go podczas procesu negocjacji szyfrowanego połączenia z serwerem. Nadal możemy pracować z zahaszowanym plikiem known_hosts
za pomocą programu narzędziowego ssh-keygen:
# Pokaż pozycję danego hosta w pliku known_hosts agresor@darkstar:~$ ssh-keygen -F darkstar.nfsec.pl # darkstar.nfsec.pl found: line 6 |1|+agd/Kaa8gU/LDfm6wJ7XZL5wm0=|dsIV75bN41abkL1+hNVhmviwd7Y= ecdsa-sha2-nistp256 AAA...5 # Pokaż pozycję danego hosta w pliku known_hosts oraz jego fingerprint agresor@darkstar:~$ ssh-keygen -l -F darkstar.nfsec.pl # Host darkstar.nfsec.pl found: line 6 darkstar.nfsec.pl ECDSA SHA256:V3TEaveUUHz4/sbWgwYt8O2TJmx/EqXlhHeYCmiFl5I # Usuń pozycję danego hosta z pliku known_hosts agresor@darkstar:~$ ssh-keygen -R darkstar.nfsec.pl # Host darkstar.nfsec.pl found: line 6 /home/users/agresor/.ssh/known_hosts updated. Original contents retained as /home/users/agresor/.ssh/known_hosts.old
Jak dokładnie działa mechanizm haszowania? Wkładamy nazwę serwera + sól do shakera SHA1 i otrzymujemy zahaszowany wynik. Pierwsze trzy znaki |1|
w identyfikatorze hosta są magicznym łańcuchem znaków (HASH_MAGIC) mówiącym nam, że wpis ten jest zahaszowany, a nie trzymany w postaci czystego tekstu. Kolejne pola (oddzielone znakiem “|”) to 160-bitowy, losowy łańcuch soli oraz 160-bitowy wynik skrótu SHA1. Oba są zakodowane w formacie base64. Spróbujmy teraz odwrócić proces haszowania:
$ ssh-keygen -F emperor.nfsec.pl # Host emperor.nfsec.pl found: line 3 |1|l6jsic+embdK6dxCSsRgCGXmyG4=|8VlHhaeExWbE8tLySTD7uyMBaO8= ecdsa-sha2-nistp256 AAA...E $ host="emperor.nfsec.pl" $ salt="l6jsic+embdK6dxCSsRgCGXmyG4=" $ salt_hexdump=$(echo $salt | base64 --decode | xxd -p) $ echo -n $host | openssl sha1 -binary -mac HMAC -macopt hexkey:$salt_hexdump | base64 8VlHhaeExWbE8tLySTD7uyMBaO8=
Wynikowy ciąg (8VlHhaeExWbE8tLySTD7uyMBaO8=) jest zakodowanym w base64 wynikiem haszowania wygenerowanym przez wprowadzenie naszej nazwy serwera i soli znalezionej w pliku. Jest to ten sam łańcuch, który za pomocą ssh-keygen odszukaliśmy w pliku known_hosts
, więc możemy być pewni, że wpis ten dotyczy hosta: emperor.nfsec.pl.
Łamanie pliku known_hosts
:
Jeśli wydamy polecenie:
ssh emperor.nfsec.pl
to do pliku known_hosts
zostaną dodane dwa wpisy. Jeden będzie odnosił się do nazwy domenowej (emperor.nfsec.pl), a drugi do adresu IP (np. 10.0.56.101). Skoro wiemy, jak odwrócić proces haszowania musimy tylko odsolić 4,294,967,296 adresów IPv4. Możemy wykorzystać do tego hashcat oraz jego atak z użyciem maski, który może być lepiej dostosowany do łamania niż czysta metoda brute force. Spróbujmy. Pierwszym krokiem jest dostosowanie formatu pliku known_hosts
do formatu, który może łamać hashcat. Możemy wykorzystać do tego skrypt w języku Python:
#!/usr/bin/env python3 import sys import base64 import re import codecs def main(kh_file): kh = readkh(kh_file) output = convertkh(kh) printOutput(output) def printOutput(output): for line in output: print(line) def convertkh(kh): output = [] for line in kh: line = (codecs.decode(codecs.encode(base64.b64decode(line[1]), "hex"), ("utf-8")) + ":" + codecs.decode(codecs.encode(base64.b64decode(line[0]), "hex"), ("utf-8"))) output.append(line) return output def readkh(kh_file): try: restring = re.compile("\|1\|(.*?)\|(.*?)\s") kh = re.findall(restring, open(kh_file).read()) except: print("Unable to open known_hosts file: %s" % kh_file) return kh if __name__ == '__main__': if(len(sys.argv) != 2): print("Usage: python3 known_hosts-converter.py [known_hosts file]") exit() else: main(sys.argv[1])
Mając skrypt wykonujemy proces konwersji:
python3 known_hosts-converter.py ~/.ssh/known_hosts > to_crack.txt
Brakuje nam jeszcze pliku z definicją masek, za pomocą którego dynamicznie wygenerujemy wszystkie adresy IPv4 podczas procesu łamania. Mając już cały komplet możemy przystąpić do łamania:
./hashcat -m 160 --hex-salt to_crack.txt -a 3 ipv4mask.txt
Plik z 34’ma wpisami udało złamać (podstawić każdą sól do każdego adresu IP) się w 1 godzinę i 42 minuty na karcie, która raportuje w teście HMAC-SHA1 (key = $salt) szybkość: 149.4 MH/s:
Started: Tue May 21 22:43:13 2019 Stopped: Wed May 22 00:25:54 2019 09340d790db3d8507cbe8f1e51896bd0ed5b1304:f560696a366f49f07d1548a1e40efba95f648d0e:\ 10.71.193.123 b8950feb7e3927a6a7d33dd880d9690d93b2b79c:00ec027da03a1d43d1a371f8e97db036dcd1f4a7:\ 10.71.193.119 9e0b35cdca572f2ff460ad61bbc011e177b44101:d528e162c04c4379d5c58872f96c7d8ff126cfb9:\ 10.71.193.113 85d1ea714f9d9840a397faa93957dfee96ebb99d:d3baec93ed6bd209121a06785a1436e4fa247c6a:\ 10.71.193.142 6131060b4d467158ced0bf144ed14784a7ab22e2:9a1b80a524548539f8580f870549fa2d90e3e382:\ 10.71.193.140 e9f758345188caa2509d30afe7c02ff8f6306b68:fe040279e0e2657186bdf9ea2787afe9decc8f28:\ 10.71.193.135 2eebaefd3b13c97be9afd31b35a4465420685c38:dc702a1469571c7972ae076f0135400ac94fa057:\ 10.71.193.146 2d9d119e64f254176e280c228805c3114ea44266:b33b64a66d1b81eb7bf16acfce85d99040af8cbb:\ 10.71.193.139 47ad4ce4659cc68e9aa6304577c19ea5bc2bda55:b4f08ac8397461451f4e48feb24274e71a1ac5e0:\ 10.71.193.127 86a627459d0c8cd17aedb76d4bb26697108c7d5e:d7cc6f3395ca289b3616dece7f57fefacfb3e269:\ 10.71.192.233 f52ea1aef7e76ac7bb8d98b14bf8ee6bc40659a0:546913f059caddc48adda3e5eb0e41b4a92da214:\ 10.71.192.246 48a112cc32874a30a93447b78c586b8e644612a6:bf9672bf830579a47adc4c499debd07a0994e909:\ 10.71.192.70 f9a02a0b9de539f5df32f9bf74814cebae85d7f7:1f1ba823e2f4d3cadf885692b8b08cf278180932:\ 10.71.192.74 bfffdec6fb475bb394dd29401f3ce262cb16b9ad:13d4ef4c798642809885a70d6ffb42be1f80e046:\ 10.71.192.76 7f4b689798f4e84bc94457a9fb3ab07f7bffc561:e3c29a65f841aefd691dd8b3fcf7c96dd75cb482:\ 10.71.192.73 570a79281973c4f3bdb21b294ac1a366661fe893:4dc7fee2f5256d0bbc142ea289ace301161d4053:\ 10.71.192.68 67db9976015d18096311e709e2b33b57d89cb1d8:462c8b430fb4c9fa512ea9c6fae1af26a1fa1ade:\ 10.0.0.1
Oczywiście powyżej znajduje się tylko 17’cie wpisów, ponieważ są to tylko złamane wpisy dla adresów IP, a nie nazw domenowych.
Podsumowanie:
Istnieje bardzo wiele głosów mówiących o nadmiarowości aktywowania opcji SSH HashKnownHosts. Jedni zwracają uwagę, że mniejszym kosztem będzie przeskanowanie całej puli adresowej, do której się dostaliśmy lub posprawdzać aktualne połączenia sieciowe maszyny (np. za pomocą netcat
). Drudzy mówią, że i tak większość wrażliwych informacji wycieka przez historię powłoki. Jeszcze inne głosy narzekają na późniejsze komplikacje związane z używaniem różnych automatów do autoryzacji za pomocą kluczy SSH – no i w ogóle skoro można to złamać to po co tego w ogóle używać? Wszystko to zależy od różnych czynników. Na przykład: skanowanie całej sieci może wywołać zbyt dużo szumu i zostać zauważone; serwer może nie posiadać wewnętrznej adresacji i tylko komunikować się z innymi po publicznej; historia wydawania poleceń przez użytkowników może być zdezaktywowana i zbierana nie z poziomu powłoki, a wywołań systemowych.
Jeśli chodzi nam o ochronę przed szkodliwym oprogramowaniem, któremu łatwiej jest dokonać obróbki czystego tekstu zapisanego w jednolitym standardzie pliku known_hosts
(niż odpalanie wyrażeń regularnych na plikach historii), a wprowadzenie tej zmiany nie komplikuje nam procesu zarządzania uwierzytelnianiem – to chyba nic nie stoi na przeszkodzie, aby dodać kolejny punkt ochrony SSH do naszego systemu.
Więcej informacji: SSH HashKnownHosts File Format, SSH: benefits of using hashed known_hosts, Why should I use “HashKnownHosts yes” in ssh_config?, known_hosts hash cracking with hashcat, SSH as a worm vector