Bug w uniksowym sudo

Błędy zdarzają się w każdym oprogramowaniu. Jedne znajdują się od razu, inne są bardziej sprytne chowają się latami. Tak się stało w przypadku sudo.

Wstęp

Ostatnim przykładem błędu, który latami nie został odkryty jest błąd w PHP. W środowisku z PHP uruchamianym przy użyciu CGI możliwe było w niektórych przypadkach na dołączenie różnych parametrów (znanych z CLI – PHP uruchamianego z wiersza poleceń). Przykładowo możliwe było wywołanie url-a http:/jakasstrona.pl/index.php?-s, który powodował zamiast wyświetlenia strony, zwrócenie pliku php – tu index.php. Wystarczy choćby zerknąć w listę parametów, aby dostrzec wiele możliwości wykorzystania tego w celu wykonania udanego włamania. Jest to przykład błędu nieodkrytego przez 8 lat.

Takie przypadki zdarzają się raz na jakiś czas. tym razem trafiło się sudo.

sudo

sudo (superuser do) to program znajdujący się w systemach UNIX i GNU/Linux (oraz innych) za pomocą którego korzystając z konta zwykłego użytkownika można wykonać polecenia z poziomu innego użytkownika. Pierwotnie (na co wskazuje nazwa) pozwalało na wykonywanie poleceń jako superużytkownik (zazwyczaj root), ale zastosowanie zostało rozszerzone też o inne konta.

Konfiguracja sudo zazwyczaj znajduje się w pliku /etc/sudoers (np. dla Debiana) i pozwala na ustawienie kont użytkowników i poleceń, którymi mogą się posługiwać przy pomocy sudo. Co więcej dostęp taki dla sesji zdalnych może być ograniczony do konkretnych adresów ip, czy podsieci. Tu właśnie pojawił się wspomniany bug.

Obrazek podpatrzony w źródłowym wpisie

bug

Jak się okazało, sudo zawierało pewien błąd, który mógł być poważny w określonych sytuacjach. Tym błędem był brak jednej linijki kodu, konkretniej 6 znaków. Ale po kolei.

W wielu językach programowania występuje wyrażenie switch, które wraz z case, default i break tworzy konstrukcję warunkową. Weźmy za przykład:

switch (variable){
    case 1:
        do_one();
        break;
    case 2:
        do_two();
        break;
    default:
        do_default();
        break;
}

Jeśli zmienna variable jest równa 1, wykonaj funkcję do_one(), jeśli 2 – do_two(), w przeciwnym wypadku wykonaj do_default().

Co więcej, break można pominąć, jeśli chcemy aby dla różnych wartości zmiennej wykonywane były wspólnie fragmenty kodu:

switch (variable){
    case 1:
        do_one();
    case 2:
        do_two();
        break;
    default:
        do_default();
        break;
}

W tym przypadku (usunięty break z pierwszego case) dla wartości 1 wykona się do_one() i do_two().

Czasami jednak brak break nie jest celowy i wtedy mogą dziać się dość nieoczekiwane rzeczy. To właśnie stało się z sudo. Jak wspomniałem, dostęp w sudo może być ograniczony do danej podsieci. Nikt spoza niej nawet jeśli zna hasło (lub co gorsza jeśli hasło jest wyłączone dla określonych poleceń) nie wykona polecenia.

W tym fragmencie kodu zabrakło break-a (tego bezpośrednio nad case AF_INET6:):

switch (family) {
    case AF_INET:
    if ((ifp->addr.ip4.s_addr & mask.ip4.s_addr) == addr.ip4.s_addr)
        debug_return(true);
    break;
    case AF_INET6:
    for (j = 0; j < sizeof(addr.ip6.s6_addr[j]); j++) {
        if ((ifp->addr.ip6.s6_addr[j] & mask.ip6.s6_addr[j]) != addr.ip6.s6_addr[j])
            break;
    )
    if (j == sizeof(addr.ip6.s6_addr))
        debug_return_bool(true);
    break;
}

…i stały się dziwne rzeczy. Okazało się, że w przypadku jeśli użytkownik usiłuje wykonać polecenie przez sudo spoza podsieci, dostaje taką możliwość. Wynika to z faktu, że po nieprzejściu testu adresu IPv4 (testu, czy adres IP znajduje się w podanej podsieci) zabrakło break-a który przerwał by dalsze wykonywanie się kodu. Kod wykonywał się dalej i sprawdzał adres IPv6, który był pusty, zatem pętla for nie wykonywała się w ogóle (ale ustawiała zmienną j na wartość zero). Dalej był jeszcze jeden warunek sprawdzający czy owa zmienna j ma taką samą wartość, jak ilość znaków w adresie IPv6, co było prawdą (zerowy rozmiar adresu IPv6 i zmienna j ustawiona na 0) – zwracane było true podobnie jak w przypadku, gdyby adres IPv4 należał do zdefiniowanej podsieci.

Konkluzja

Na informację tą trafiłem na blogu Sophos. Autor zawarł tam kilka ciekawych wniosków. M.in. zawsze należy wstawiać break; na końcu switch-a – nie jest potrzebny, ale deweloper, który dopisze coś dalej nie zapomni o jego wstawieniu – nie będzie musiał nawet pamiętać.

Mi się zdarzyły już takie sytuacje, teraz tym bardziej będę wyczulony :)

 

 

Źródo: Sophos
Obrazki: [1], [2]

Ten wpis został opublikowany w kategorii Debian, Linux, Oprogramowanie, Programowanie, Serwery i oznaczony tagami , . Dodaj zakładkę do bezpośredniego odnośnika.

Dodaj komentarz

Musisz się zalogować (także Facebook, Google+, Twitter), aby móc dodać komentarz.