Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
OpenSSH – enumeracja użytkowników – CVE-2018-15473
Wprowadzanie
Podczas testów bezpieczeństwa infrastruktury często stajemy przed zadaniem przetestowania bezpieczeństwa serwera SSH. Jednym z podstawowych testów jest sprawdzenie odporności na ataki brute-force. W atakach tego typu znajomość poprawnych nazw użytkowników w znacznym stopniu zwiększa prawdopodobieństwo sukcesu – zamiast sprawdzać wszystkie możliwe kombinacje potencjalnych nazw użytkowników i haseł, sprawdzamy poprawność haseł tylko dla istniejących użytkowników. Jednym ze sposób na uzyskanie listy poprawnych nazw użytkowników jest enumeracja, która w dużym uproszczeniu sprowadza się do wykorzystania błędu serwera uwierzytelniającego w celu weryfikacji czy dana nazwa użytkownika jest poprawna. Jeżeli serwer uwierzytelniający pozwala na enumerację nazw użytkowników, atak brute-force możemy rozbić na dwa etapy:
- Listę potencjalnych nazw użytkowników filtrujemy odpytując serwer o poprawność danej nazwy
- Listę istniejących nazw użytkowników używamy do przeprowadzenia właściwego ataku brute-force
Z uwagi na popularność serwera OpenSSH postanowiłem przeanalizować kod serwera pod kątem podatności na enumerację nazw użytkowników. Analizie poddana została najnowsza (na dzień 2018.07.18) wersja OpenSSH 7.7p1.
Szczegóły techniczne
Plik auth2-pubkey.c zawiera kod implementujący uwierzytelnienie za pomocą klucza:
static int userauth_pubkey(struct ssh *ssh) { Authctxt *authctxt = ssh->authctxt; struct passwd *pw = authctxt->pw; struct sshbuf *b; struct sshkey *key = NULL; char *pkalg, *userstyle = NULL, *key_s = NULL, *ca_s = NULL; u_char *pkblob, *sig, have_sig; size_t blen, slen; int r, pktype; int authenticated = 0; struct sshauthopt *authopts = NULL; if (!authctxt->valid) { debug2("%s: disabled because of invalid user", __func__); return 0; } if ((r = sshpkt_get_u8(ssh, &have_sig)) != 0 || (r = sshpkt_get_cstring(ssh, &pkalg, NULL)) != 0 || (r = sshpkt_get_string(ssh, &pkblob, &blen)) != 0) fatal("%s: parse request failed: %s", __func__, ssh_err(r)); pktype = sshkey_type_from_name(pkalg);
W przypadku, gdy próba uwierzytelnienia odbywa się dla nieistniejącego użytkownika warunek sprawdzany w linii 101 jest spełniony w rezultacie serwer kończy proces uwierzytelnienia (linia 103). W przeciwnym przypadku, gdy nazwa użytkownika jest prawidłowa, proces uwierzytelnienia jest kontynuowany.
Linie 105-107 zawierają kod odpowiedzialny za parsowanie pakietu SSH. Jeżeli nastąpi błąd parsowania następuje awaryjne zakończenie procesu (wywołanie funkcji „fatal” w linii 108). Na tym etapie analizy możemy założyć, że „łagodne” (linia 103) i awaryjne (linia 108) zakończenie procesu uwierzytelnienia powinno być, z perspektywy klienta SSH, jednoznacznie rozróżnialne.
W celu opracowania sposobu na awaryjne zakończenie procesu przeanalizujmy funkcję „sshpkt_get_string” (packet.c):
int sshpkt_get_string(struct ssh *ssh, u_char **valp, size_t *lenp) { return sshbuf_get_string(ssh->state->incoming_packet, valp, lenp); }
W linii 2521 wywoływana jest funkcja „sshbuf_get_string” (sshbuf-getput-basic.c):
int sshbuf_get_string(struct sshbuf *buf, u_char **valp, size_t *lenp) { const u_char *val; size_t len; int r; if (valp != NULL) *valp = NULL; if (lenp != NULL) *lenp = 0; if ((r = sshbuf_get_string_direct(buf, &val, &len)) < 0) return r;
W linii 107 wywoływana jest funkcja „sshbuf_get_string_direct” (sshbuf-getput-basic.c):
int sshbuf_get_string_direct(struct sshbuf *buf, const u_char **valp, size_t *lenp) { size_t len; const u_char *p; int r; if (valp != NULL) *valp = NULL; if (lenp != NULL) *lenp = 0; if ((r = sshbuf_peek_string_direct(buf, &p, &len)) < 0) return r; if (valp != NULL)
W linii 134 wywoływana jest funkcja „sshbuf_peek_string_direct” (sshbuf-getput-basic.c):
int sshbuf_peek_string_direct(const struct sshbuf *buf, const u_char **valp, size_t *lenp) { u_int32_t len; const u_char *p = sshbuf_ptr(buf); if (valp != NULL) *valp = NULL; if (lenp != NULL) *lenp = 0; if (sshbuf_len(buf) < 4) { SSHBUF_DBG(("SSH_ERR_MESSAGE_INCOMPLETE")); return SSH_ERR_MESSAGE_INCOMPLETE; } len = PEEK_U32(p); if (len > SSHBUF_SIZE_MAX - 4) { SSHBUF_DBG(("SSH_ERR_STRING_TOO_LARGE")); return SSH_ERR_STRING_TOO_LARGE; } if (sshbuf_len(buf) - 4 < len) { SSHBUF_DBG(("SSH_ERR_MESSAGE_INCOMPLETE")); return SSH_ERR_MESSAGE_INCOMPLETE; }
Funkcja wykonuje walidację wartości typu „string”, sprawdzana jest między innymi długość danych (linia 165) – długość nie może być większa niż SSHBUF_SIZE_MAX – 4 .
Stała SSHBUF_SIZE_MAX zdefiniowana jest w pliku sshbuf.h:
#define SSHBUF_SIZE_MAX 0x8000000 /* Hard maximum size */
W tym momencie uzyskaliśmy sposób na zmuszenie funkcji „sshpkt_get_string” do zwrócenia błędu – wysłanie wartości typu „string” o długości większej niż 0x8000000 – 4 .
Wróćmy do analizy funkcji „userauth_pubkey” (auth2-pubkey.c):
static int userauth_pubkey(struct ssh *ssh) { Authctxt *authctxt = ssh->authctxt; struct passwd *pw = authctxt->pw; struct sshbuf *b; struct sshkey *key = NULL; char *pkalg, *userstyle = NULL, *key_s = NULL, *ca_s = NULL; u_char *pkblob, *sig, have_sig; size_t blen, slen; int r, pktype; int authenticated = 0; struct sshauthopt *authopts = NULL; if (!authctxt->valid) { debug2("%s: disabled because of invalid user", __func__); return 0; } if ((r = sshpkt_get_u8(ssh, &have_sig)) != 0 || (r = sshpkt_get_cstring(ssh, &pkalg, NULL)) != 0 || (r = sshpkt_get_string(ssh, &pkblob, &blen)) != 0) fatal("%s: parse request failed: %s", __func__, ssh_err(r)); pktype = sshkey_type_from_name(pkalg);
W linii 107 następuje próba odczytania wartości typu „string”. Wysłanie wartości o długości większej niż 0x8000000 – 4 powinno spowodować, że funkcja „sshpkt_get_string” zwróci błąd, a w rezultacie nastąpi awaryjne zakończenie procesu (linia 108). Warto wspomnieć, że wysłane dane nie muszą mieć długości większej niż 0x8000000 – 4 , tylko zadeklarowany rozmiar (przesyłany w osobnym polu) musi przekraczać dozwoloną wartość.
W celu wysłania spreparowanego pakietu SSH, który zawiera „string” z odpowiednią długością zmodyfikuję bibliotekę paramiko. Paramiko to implementacja protokołu SSHv2 napisana w Pythonie (https://github.com/paramiko/paramiko). Zachęcam do modyfikacji biblioteki we własnym zakresie.
Na bazie zmodyfikowanej biblioteki paramiko napiszmy prosty skrypt do sprawdzenia poprawności nazwy użytkownika:
import paramiko import sys ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(hostname=sys.argv[1], port=int(sys.argv[2]), username=sys.argv[3], key_filename='./conf/id_rsa', password='', look_for_keys=False) ssh.close()
Uruchomienie skryptu dla nazwy istniejącego użytkownika (user) kończy się błędem „No existing session”
python2 test.py debian 22 user [...] paramiko.ssh_exception.SSHException: No existing session
Natomiast uruchomienie skryptu dla nazwy nieistniejącego użytkownika (asdf) kończy się błędem „Authentication failed.”
python2 test.py debian 22 asdf [...] paramiko.ssh_exception.AuthenticationException: Authentication failed.
Różnica w odpowiedzi serwera pozwala jednoznacznie stwierdzić czy użytkownik o danej nazwie istnieje w testowanym systemie.
Na zakończenie warto wspomnieć, że przedstawiony błąd enumeracji oprócz oczywistego wykorzystania w pierwszej fazie ataku brute-force, może zostać również wykorzystany do detekcji oprogramowania zainstalowanego na testowanym serwerze np. wykrycie użytkownika „postgres” sugeruje, że wykorzystywana jest baza PostgreSQL. Idąc dalej istnienie lub brak określonych nazw może sugerować użycie danego systemu operacyjnego.
Podsumowanie
Błąd został zgłoszony do zespołu odpowiedzialnego za rozwój OpenSSH. Z uwagi na fakt, że wersja OpenSSH, która zawiera poprawkę nie została jeszcze opublikowana, natomiast w sieci można znaleźć działający exploit, rekomendujemy wdrożyć następujące kroki w celu ochrony przed atakami typu brute-force:
- jeśli to możliwe należy wdrożyć mechanizm uwierzytelnienia oparty na kluczach
- upewnić się, że konta zabezpieczone są silnymi hasłami
- rozważyć wdrożenie mechanizmów utrudniających przeprowadzenie ataków typu brute-force np. fail2ban
Timeline
16.07.2018 – zgłoszenie błędu do zespołu OpenSSH
18.07.2018 – potwierdzenie błędu przez zespół OpenSSH
31.07.2018 – publikacja poprawki na githubie
15.08.2018 – na bazie publicznie dostępnej poprawki opublikowane zostały, przez niezależnego badacza, szczegóły techniczne błędu (http://seclists.org/oss-sec/2018/q3/124)
17.08.2018 – przypisany został nr CVE-2018-15473
22.08.2018 – nasza publikacja
Dariusz Tytko, realizuje testy bezpieczeństwa w Securitum
Coś pięknego! Dobra robota!
Naprawdę dobrze opisane, tutaj coś odnośnie tej podatności: https://github.com/Rhynorater/CVE-2018-15473-Exploit oraz https://github.com/trimstray/massh-enum
Bardzo fajna podatność :) Jakieś info na temat podatnych wersji oprócz wymienionej?
Wszystkie wersje openssh az do wersj 2.x.
widziałem dyskusje na reddicie ^^
fail2ban i nie trzeba wczytywac się w artykuł ;-)
No i nie zapominaj o portknockingu, kluczu sprzętowym, wszystko w DMZ, IPS/IDS, regułkach iptables… o czymś zapominałem? :P
Będzie strona internetowa i logo ?
Jeszcze koszulki i piosenka!
@lol poważnie pytasz…?
logo, strona, opis tworzysz jak błąd jest medialny i z punktu widzenia bezpieczeństwa KRYTYCZNY.
Przykład:
-spectre
– meltdown
Poważnie jak zawsze :D
Ogólnie sądzę, że wszelkie raporty i dokumenty powinny mieć dziś postać multimedialną. Na przykład raporty powinny być prezentowane w formie nagrań na których wynajęci modele lub prezenterzy telewizyjni w towarzystwie tańczących modelek przy dźwiękach energicznej muzyki opowiadają o podatnościach językiem zrozumiałym dla szerokiej rzeszy odbiorców.
Należy iść z duchem czasu, a przecież żyjemy w epoce streamingu, wiecznego pędu, smartfonów, łatwej informacji i czy tego chcemy czy nie dokumenty pisane stają się reliktem przeszłości stającym w opozycji do postępu.
WIDEO JEST ZŁEM!!!!!
Pięć razy szybciej jest przeczytać.
To powinien być występ, wykon. Tak żeby na koniec Jury mogło wstać i powiedzieć „Tak ! Jestem na tak, podoba mi się twój wykon!”
Wiecie, rozumiecie ?
Dopóki społeczność nie dojrzeje do nowej formy wyrazu to nigdy nie uda się oswoić z bezpieczeństwem mas ^^
@lol,
jak piszesz poważnie to szacun.
Jak jestem zdania że podatności, dziury o mniejszej „wadze” powinny być po prostu opisane, krytyczne można zrobić „szoł” o którym piszesz.
W natłoku różnych dziur, podatności, nadużyć straciłbym osobiście rozpoznanie co i jak.
eh nie rozumiesz współczesnej sztuki.
Wszystko można zarapować
Jezeli w sieci od pewnego czasu jest exploit (cos co juz wykorzystuje podatnosc) analiza oraz odtworzenie podatnosci nie jest problemem. Czy takie zgloszenie ma wartosc (zakladam ze ktos juz to zglosic przed Wami).
Nasze zgłoszenie jest tym pierwotnym (co wynika też z opisu linkowanego w tekście commitu).
Hasło? A W jakim celu skoro w sshd_config możemy wyłączyć logowanie po haśle.
Dodalbym jeszcze „Implement ACL list” do rekomendacji, ale fail2ban poprawnie skonfigurowany powinien zalatwic sprawe…
good job
Nie chciałbym absolutnie dyskredytować tutaj Was, ale ten „błąd” to burza w szklance wody. Widzę, że Solar Designer już Wam odpisał, ale w zasadzie uważam podobnie. Jest to jakieś issue, ale średnio ważne – jest multum innych sposobów „zgadnąć” userów. W zasadzie 80% nie trzeba zgadywać – wystarczy sprawdzić dockery/vagranty oficjalnych dystrybucji i zgadnięte od kopa. No i co? Ano nic.
User do MySQLa/PgSQLa? Zwykle wystarczy sprawdzenie *:3306 i *:5432 i czasem tu można więcej szkód narobić, niż wbijać do powłoki, która notabene może być /bin/false..
Można wymieniać i wymieniać. Generalnie w dzisiejszych czasach ten „błąd” jest ciężko do czegoś wykorzystać. Instancje są zwykle w cloudzie, krótko żyją, a na dobitkę, mogą mieć losowe nazwy userów (typu iewfjiewfjo3t53809t433, dodanych do AllowUsers w sshd_config i autoryzowanych kluczem), losowy port ssh (>1024) i SELinux+ekstra hasło do sudo. Dzięki Vault/Consul dziś takie „twory” są proste i łatwo zarządzalne i generalnie ja do swoich instancji nie znam portu ssh, usera, ani nawet często hosta (niech mi consul zgubi bazę to będę miał)
Także nie wiem czy to na CVE w ogóle zasługuje, gdybyście jakiś RCE lub DoS znaleźli to rozumiem (ale to wtedy co drugi serwis by o Was wspominał). Moim zdaniem, znacznie groźniejszego kalibru problemem był błąd znaleziony przez lcamtufa parę miesięcy temu w sftp: https://securitytracker.com/id/1039508
Pozdrawiam,
Mateusz, ale my absolutnie nigdzie nie piszemy że to jakieś nie wiadomo hiper co. Po prostu zgłosiliśmy podatność do OpenSSH, podziękowali i sfiksowali. Nie pisaliśmy do zagranicznej prasy, a nawet nie występowaliśmy o CVE :)
Z innej strony na reddicie (netsec) ta podatność obecnie jest jednym z najbardziej plusowanych tematów. Polecam zresztą dyskusję: https://www.reddit.com/r/netsec/comments/97w102/openssh_username_enumeration/
Z innej strony ktoś (Qualys) próbuje podłączyć się pod temat i zdobyć medialne zainteresowanie – patrz twit Stefana Essera, który niejako nas bierze w obronę: https://twitter.com/i0n1c/status/1032615978180530180
CVE. Ludzie zwrócili też uwagę, że trudniejsze do wykorzystania enumeracje w OpenSSH dostawały CVE:
http://seclists.org/oss-sec/2018/q3/124
(this) is easier to exploit than previous
OpenSSH username enumerations (which were all timing attacks):
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2003-0190
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2006-5229
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-6210
Sam zresztą exploit na ten nasz temat dostał na Twitterze prawie 500 retwittów, więc chyba to jakoś ludzi interesuje: https://twitter.com/Rhynorater/status/1031715914197225475
Nie wiem trochę jaki tak naprawdę jest wydźwięk Twojego komentarza, następnym razem mamy schować takie badanie do kieszeni i przygotować exploit na własne potrzeby? (może skorzystamy z takiej ścieżki :)
Co do innych CVE, to mieliśmy ostatnio coś w Cisco ASA (szczegóły do poszukania na sekuraku):
https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20180606-asaftd
Cisco dało Score 8.6/10. Też lepiej było tego nie zgłaszać i „nie zasługuje to na CVE” bo nie mamy RCE w Cisco ASA? :-)
PS
Pokaż jakieś swoje fajne podatności to podyskutujemy dalej :-)
Ja osobiście uważam że fajnie że zgłosiliście ten bład, nie ważna jest krytyczność a sam fakt że jest podatność i można ją wykorzystać (nie ważne jest prawopodobieństwo tego zdarzenia).
Oby więcej takich informacji bo to napędza rozwój.
Przy okazji chciałem tylko przypomnieć (zgadzasz się @sekuraku?), że to nie jest tak że zawsze siedzisz nad kodem cały wolny czas i szukasz dziur w kodzie aplikacji, nieraz jest to przypadek że znajdujesz błąd i dopiero teraz zaczyna się zabawa. :)
Panowie M lub D (bo nie wiem, który napisał komentarz jako „sekurak” :))
Tak jak wspomniałem nie chcę dyskredytować, poddaję tylko w wątpliwość severity tego błędu, z drobnym przemyśleniem. Ja czytam Wasze dokonania i doskonale wiem, że stać Was na dużo więcej, jak również doceniam fakt przyczyniania się do poprawy bezpieczeństwa tak istotnego softu jak OpenSSH.
Po prostu rzucił mi się w oko Was opis, do czego by to można wykorzystać, który mnie trochę rozbawił. :-) Oczywiście, możemy zawęzić dyskusje do jakichś starych serwerów, na których userzy mają hasła typu „123456”, w tym, ten „trudnozgadywalny” user „root” również (bo w niektórych systemach jest PermitRootLogin=yes), port jest standardowy, ale takie serwery są już „obstawione” przez boty, które regularnie takie wyszukują, więc nawet tutaj, ta podatność niewiele zmienia.
No generalnie po którymś Waszym newsie (oferta pracy?) napisaliście o tym tak tajemniczo, że myślałem, że chodziło o jakiegoś killera… to wstrzymywanie publikacji itp… a tu taki zawód. :)
Na Twittery i Reddity bym się nie powoływał (w tym na ilość plusów), bo to nie festiwal ludowy.
Pozdrawiam,
A czy my pisaliśmy gdziekolwiek o severity? Jeszcze gdyby w tytule było coś a la – „poważna” podatność. A tu nawet tego nie ma.
Z innej strony – fakt że przez prawie 20 lat nikt tego problemu nie wyłapał w OpenSSH o czymś świadczy :-P
„Instancje są zwykle w cloudzie”.
a-a, obawiam się, że kolega polskiego legacy nie widział.
Jak najbardziej – nieraz przystępowałem do analizy kodu dopiero po tym, gdy zauważyłem, że aplikacja „dziwnie” się zachowuje. Natomiast w tym konkretnym przypadku skupiłem się na analizie kodu pod kątem podatności na enumerację. Analiza kodu to obszerne zagadnienie. Wbrew pozorom istnieje co najmniej kilka, bardziej efektywnych sposobów niż czytanie kodu linijka po linijce. Czwarty rozdział „Application Review Process” książki „The Art of Software Security Assessment: […]” to świetne źródło informacji na ten temat.