Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book

OpenSSH – enumeracja użytkowników – CVE-2018-15473

22 sierpnia 2018, 12:05 | Teksty | komentarzy 27
Poniższy artykuł planowaliśmy opublikować po udostępnieniu wersji OpenSSH zawierającej stosowne poprawki. Jednak z uwagi na fakt, że w Internecie pojawiły się szczegółowe informacje opisujące problem wraz z działającym exploitem, postanowiliśmy przyśpieszyć publikację.

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:

  1. Listę potencjalnych nazw użytkowników filtrujemy odpytując serwer o poprawność danej nazwy
  2. 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

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. Adam

    Coś pięknego! Dobra robota!

    Odpowiedz
  2. dave
    Odpowiedz
  3. Eddie Murphy

    Bardzo fajna podatność :) Jakieś info na temat podatnych wersji oprócz wymienionej?

    Odpowiedz
    • Wszystkie wersje openssh az do wersj 2.x.

      Odpowiedz
  4. oioi

    widziałem dyskusje na reddicie ^^

    Odpowiedz
  5. czesław

    fail2ban i nie trzeba wczytywac się w artykuł ;-)

    Odpowiedz
    • leszław

      No i nie zapominaj o portknockingu, kluczu sprzętowym, wszystko w DMZ, IPS/IDS, regułkach iptables… o czymś zapominałem? :P

      Odpowiedz
  6. lol

    Będzie strona internetowa i logo ?

    Odpowiedz
    • teos

      Jeszcze koszulki i piosenka!

      Odpowiedz
    • olo

      @lol poważnie pytasz…?
      logo, strona, opis tworzysz jak błąd jest medialny i z punktu widzenia bezpieczeństwa KRYTYCZNY.
      Przykład:
      -spectre
      – meltdown

      Odpowiedz
      • lol

        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.

        Odpowiedz
        • Krzysztof Kozłowski

          WIDEO JEST ZŁEM!!!!!
          Pięć razy szybciej jest przeczytać.

          Odpowiedz
      • lol

        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 ^^

        Odpowiedz
        • olo

          @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.

          Odpowiedz
          • lol

            eh nie rozumiesz współczesnej sztuki.
            Wszystko można zarapować

  7. Ktocojak

    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).

    Odpowiedz
    • Michał Bentkowski

      Nasze zgłoszenie jest tym pierwotnym (co wynika też z opisu linkowanego w tekście commitu).

      Odpowiedz
  8. Konrad

    Hasło? A W jakim celu skoro w sshd_config możemy wyłączyć logowanie po haśle.

    Odpowiedz
  9. Adam

    Dodalbym jeszcze „Implement ACL list” do rekomendacji, ale fail2ban poprawnie skonfigurowany powinien zalatwic sprawe…

    Odpowiedz
  10. G

    good job

    Odpowiedz
  11. Mateusz

    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,

    Odpowiedz
    • 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 :-)

      Odpowiedz
      • Jamysletak

        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. :)

        Odpowiedz
      • Mateusz

        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,

        Odpowiedz
        • 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

          Odpowiedz
    • gt

      „Instancje są zwykle w cloudzie”.

      a-a, obawiam się, że kolega polskiego legacy nie widział.

      Odpowiedz
  12. Dariusz Tytko

    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.

    Odpowiedz

Odpowiedz