Java RMI dla testerów penetracyjnych: struktura, rozpoznanie i komunikacja (non-JMX Registries).

Celem tego artykułu jest wyjaśnienie, jakie interfejsy RMI (Java Remote Method Invocation) możesz napotkać podczas testowania penetracyjnego infrastruktury. Ponieważ cały temat, który chciałbym omówić, jest dość obszerny, podzieliłem go na dwie części. W kolejnej części krótko wyjaśnię, czym są interfejsy RMI, jak stworzyć jeden w celach testowych oraz jak ręcznie zbudować klienta RMI do wywołania zdalnych metod. Część dotycząca ataków została opisana w drugiej części tego artykułu, która można znaleźć pod adresem TUTAJ.

Ponadto, te artykuły dotyczą natywnych rejestrów RMI. Istnieją również popularne rejestry JMXRMI, które są nieco inne. Planuję opublikować oddzielny artykuł na temat JMX, który będzie obejmował m.in. JMXRMI i inne sposoby interakcji z Java Management eXtension. Więc krótko mówiąc, opiszę tutaj:

  • Czym są interfejsy RMI
  • Jak zbudować interfejs RMI z kodu źródłowego.
  • Jakie informacje o interfejsie RMI można uzyskać przy użyciu skanowania Nmap
  • Jak zbudować klienta RMI (i co trzeba wiedzieć, aby go zbudować)
  • Jakie są typowe problemy / stack trace przy pracy z RMI i co może być ich przyczyną

Co to Java RMI

Serwer Java RMI to wirtualna jednostka wystawiona w sieci, która umożliwia innym zdalnym klientom wywoływanie metod na systemie (technicznie rzecz ujmując na JVM działającej na tym systemie), na którym działa serwer. To nic nadzwyczajnego w świecie programowania, gdzie podobne koncepcje, jak Remote Procedure Call (RPC), są powszechnie stosowane.

Dlatego, uruchamiając wystawiony serwer Java RMI na systemie, można pozwolić zewnętrznym podmiotom na interakcję z nim, oraz możliwe jest wywoływanie metod na serwerze Java RMI. Te metody powinny być zdefiniowane w implementacji serwera. Kiedy zostaną wywołane przez klienta, będą one wykonywane na serwerze, a wyniki zostaną zwrócone do klienta. Innym interesującym aspektem jest to, że natywny RMI (ponownie, nie mówię o JMXRMI) nie oferuje zbyt wielu zabezpieczeń, poza szyfrowaniem połączenia za pomocą SSL. [1]

Architektura interfejsu RMI jest przedstawiona poniżej:

https://afine.com/wp-content/uploads/2023/07/remote-method-invocation.jpg

Nazwy „stub” i „skeleton” mogą być mylące na pierwszy rzut oka, ale to po prostu nazwy dla „części klienta” i „części serwera” obiektu.

Stub to klasa implementująca zdalny interfejs i pełniąca rolę miejsca trzymania po stronie klienta dla zdalnego obiektu. Z kolei Skeleton to jednostka po stronie serwera, która przekierowuje wywołania do rzeczywistej implementacji zdalnego obiektu.

Rejestr RMI (RMI registry) to narzędzie w języku Java, które można znaleźć w plikach binarnych JDK pod nazwą „rmiregistry”. Uruchomienie tego pliku binarnego z argumentem numerycznym, który jest portem nasłuchu (domyślnie 1099), pozwoli na zbindowanie zdalnych obiektów w rejestrze. Te zdalne obiekty mogą być później dostępne z sieci spoza maszyny, na której uruchomiony jest rejestr RMI.

W dużym uproszczeniu: najpierw uruchamiamy Rejestr RMI, a następnie tworzymy obiekt w języku Java (klasę Javy, która ma pewne metody, które mogą być wywoływane przez zdalne strony) i nadajemy mu nazwę pod którą będzie można go znaleźć w rejestrze.

Aby umożliwić zdalnym stronom wywołanie pewnych metod, rejestr RMI musi składać się co najmniej z dwóch komponentów programistycznych:

  • Interfejs, który definiuje, jakie metody będą wywoływalne na zdalnym obiekcie.
  • Kod do zbindowania i eksportowania zdalnych obiektów (metody zdalne będą wywoływane na tym obiekcie).
  • Implementacja metod zdalnych.

Budowa Klienta i Serwera RMI

Poniżej zaprezentowałem prosty przykład serwera i klienta RMI. Na początek, będą dwie niezależne klasy dla logiki serwera. Pełny kod znajdziesz także w moim repozytorium na GitHubie TUTAJ.

//RMIInterface.java
import java.rmi.*;
import java.rmi.registry.*;
import java.net.*;
interface RMIInterface extends Remote {
  public String echo(Object obj) throws RemoteException;
}
//Server.java
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.Naming;
public class Server extends UnicastRemoteObject implements RMIInterface {
  public String echo(Object input) throws RemoteException {
    return“ Echoing: “+input.toString();
  }
  protected Server() throws RemoteException {
    super();
  }
  public static void main(String[] args) {
    try {
      System.out.println(“[+] Trying to bind…”);
      //Below IP:PORT can be changed
      Naming.rebind(“rmi: //127.0.0.1:11099/RMIInterface”, new Server());
        System.out.println(“[+] Server started.”);
      }
      catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

Oba te pliki powinny być przechowywane w tym samym folderze. Teraz, skompiluj je i uruchom program „rmiregistry”. Zauważ, że powinieneś uruchomić „RMIRegistry” z tego samego katalogu, w którym znajdują się te pliki.

Rozpoznawanie interfejsu RMI za pomocą Nmapa

W tym momencie możemy użyć narzędzia nmap do przeskanowania interfejsu localhost, gdzie rejestry rmi i wszystkie powiązania są widoczne:

nmap -sV -p 11099 -T4 -A localhost

Wynik skryptu nmap o nazwie rmi-dumpregistry. Informuje nas, że:

  • Rejestr działa na porcie 11099.
  • Korzysta z RMIInterface, który jest własną klasą i nie znamy jego struktury z czysto blackboxowej perspektywy.
  • InvocationHandler działa na porcie 34087. InvocationHandler to w skrócie punkt końcowy, który zajmuje się wykonywaniem zdalnie wywoływanych metod.

Poniższy obrazek przedstawia kolejność interakcji z rejestratorem RMI:

Tworzenie Klienta RMI

Dla pełnego obrazu, zaimplementujmy kod klienta. Plik Client.java znajduje się w tym samym katalogu co Server i RMIInterface.

//Client.java
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class Client implements RMIInterface {
  public String echo(Object input) throws RemoteException {
    return“ Echoing: “+input.toString();
  }
  private static RMIInterface look_up;
  public static void main(String[] args)
  throws MalformedURLException, RemoteException, NotBoundException {
    look_up = (RMIInterface) Naming.lookup(“rmi: //127.0.0.1:11099/RMIInterface”);
      System.out.println(“Calling Echo…“);
      try {
        String response = look_up.echo(“Let’ s use a string here.“);
        System.out.println(response);
      } catch (Exception e) {}
    }
  }

Zauważ, że Client.java implementuje interfejs, który był używany po stronie serwera, a obecnie dostarczyliśmy taką samą implementację funkcji echo, jaką posiadamy po stronie serwera.

Po skompilowaniu można zobaczyć, że klient jest gotowy do użycia. Jeśli przyjrzymy się Wiresharkowi podczas uruchamiania klienta, potwierdzimy powyższy schemat, ponieważ obserwujemy najpierw nawiązanie połączenia z rejestracją RMI, a następnie drugie połączenie z InvocationHandlerem.

Na początku klient komunikuje się tylko z portem rejestru, czyli portem 11099.

Później klient otrzymuje instrukcję, żeby ponownie się skomunikować z InvocationHandlerem, który będzie obsługiwał wykonanie metody.

Możesz zauważyć, że wynik metody echo jest zwracany z tego punktu końcowego.

Warto również wspomnieć, że możemy zobaczyć, że dane są wymieniane w formie zserializowanej, co można stwierdzić, patrząc na zserializowane „magiczne bajty” 0xaced0005. Będzie to omówione bardziej szczegółowo w drugiej części artykułu na temat atakowania rejestrów RMI (Spoiler: nowoczesne wersje Javy już zapobiegają takim atakom)

W skrócie, aby połączyć się i wykonać metodę na zdalnym serwerze, potrzebowaliśmy:

  • Kod interfejsu, którego używa serwer — który nie jest publicznie widoczny ani możliwy do pobrania z rejestru.
  • Nazwa powiązania (zbindowania) i port rejestru, które zostały wykryte przez nmap.
  • Invocation handlera, który musi być dostępny na poziomie sieciowym pod adresem IP / nazwą hosta, którą przedstawia w rejestrze.

Rozwiązywanie problemów z klientem

Poniżej omówimy dwa powszechne problemy, na jakie można natrafić na tym etapie: „Foreign InvocationHandler” i brak interfejsu po stronie serwera.

Jak pokazano powyżej na zrzucie z Wiresharka, podczas łączenia się z RMI Registry, najpierw odwiedzimy rejestr, a następnie klient zostanie przekierowany do odpowiedniego InvocationHandlera. Jeśli InvocationHandler jest ustawiony na obcy host, na przykład localhost lub host o nierozpoznawalnej nazwie, pamiętaj, że to nadal Twoja maszyna będzie próbowała połączyć się z tym adresem.

  • Jeśli jest ustawiony na localhost, otrzymasz błąd połączenia, ponieważ po połączeniu się z rejestracją, klient spróbuje połączyć się z InvocationHandlerem na localhost twojej maszyny. Musisz skonfigurować przekierowanie za pomocą firewalla lub narzędzia takiego jak socat i przekierować ruch z [localhost]:[port InvocationHandlera] do [rmi_host]:[port InvocationHandlera].
  • Jeśli jest ustawiony na nieprawidłową nazwę hosta, spróbuj dodać adres IP serwera RMI wraz z nieprawidłową nazwą hosta do pliku /etc/hosts, aby została ona poprawnie rozwiązana, co pozwoli Ci na połączenie z nią.

Kolejnym problemem, który może stanąć na Twojej drodze do wykonania zdalnych metod, jest brak kodu interfejsu po stronie serwera. Jeśli nie masz do niego dostępu, atak staje się bardziej skomplikowany.

Co zrobić, jeśli nieznany jest interfejs docelowego serwera?

W drugiej części artykułu, która będzie wkrótce opublikowana, pokażę narzędzie RMIScout, które umożliwia zautomatyzowanie tego procesu. Jednak na razie skupimy się na podejściu manualnym (uważam, że ręczne podejście zawsze powinno być pierwsze, aby zrozumieć, co dzieje się za kulisami; dopiero potem można skutecznie korzystać z podejścia zautomatyzowanego).

Rozważmy poniższy przykład: Uruchomimy wyżej przedstawiony rmiserver i skopiujemy klasę klienta oraz interfejsu do innego folderu.

W takim przypadku, jeśli jesteś w stanie zgadnąć nazwę metody i argumenty, będziesz w stanie ją wywołać. Przyjrzyjmy się poniższemu kodowi klienta:

//Client2.java
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class Client2 implements RMIInterface {
  public String echo(Object something) throws RemoteException {
    String notUsed = something.toString();
    return“ Sorry, I don’ t know the original implementation”;
  }
  private static RMIInterface look_up;
  public static void main(String[] args)
  throws MalformedURLException, RemoteException, NotBoundException {
    look_up = (RMIInterface) Naming.lookup(“rmi: //127.0.0.1:11099/RMIInterface”);
      System.out.println(“Calling Echo…“);
      try {
        String response = look_up.echo(“Let’ s use a string here.“);
        System.out.println(response);
      } catch (Exception e) {}
    }
  }

Jeśli uruchomisz taki kod klienta, zostanie on wykonany, a implementacja serwera zostanie uruchomiona. Oznacza to, że do wykonania zdalnych metod wystarczy, że znasz nazwę interfejsu wraz z nazwami metod, typami zwracanych danych i typami argumentów. Dlaczego to jest ważne? Otóż jeśli jesteś w stanie uzyskać informacje o przynajmniej jednej funkcji, która jest obecna po stronie serwera, możesz skonstruować atrapę interfejsu o tej samej nazwie, którą odczytałeś z wyniku skanu nmap, umieścić w niej odpowiednią sygnaturę funkcji (typ zwracanych danych, nazwę i typy argumentów) i utworzyć klienta, który implementuje ten interfejs wraz z atrapą tej funkcji.

Ważne tylko, aby interfejs był obecny na ścieżce klasy klienta, więc może być konieczne utworzenie atrapy pakietu, na przykład org.company.rmipackage.RMIInterface, umieszczenie go w pliku .jar i w końcu dodanie go do ścieżki klasy. Po wykonaniu takiej operacji będziesz w stanie wywołać metodę po stronie serwera i otrzymać jej właściwy wynik (jeśli oczywiście metoda zwraca jakieś wartości).

To z kolei pokazuje, że wystarczy zgadnąć sygnatury metod (typ zwracanej wartości, nazwa metody i typy argumentów), aby móc je wywołać. Jeśli masz szczęście, możesz znaleźć kompletny interfejs RMI na GitHubie, co pozwoli Ci także zobaczyć, co dana metoda robi. W przeciwnym razie można wykorzystać narzędzie RMIScout, o którym będziemy dyskutować w drugiej części tego artykułu. Jeśli uda Ci się zidentyfikować metody RMI, niektóre z nich mogą już pomóc Ci zdobyć podstawowe informację o docelowym systemie.

Pełny kod użytych tu programów można odnaleźć na GitHubie TUTAJ.

Rozwiązywanie problemów z RMI Registries

Podczas pracy z rejestracjami RMI (RMI Registries) opisanymi powyżej, lub z dowolnym innym narzędziem łączącym się ze zdalnym rejestratorem (np. ysoserial lub rmiscout itp.), możesz napotkać błąd, który często jest poprzedzony tzw. „stack trace”. Chociaż stack trace jest pomocny do zrozumienia, co poszło nie tak, doskonale rozumiem, że możesz nie chcieć zagłębiać się w rozwiązywanie problemów i po prostu chcesz, aby narzędzie działało i przejść dalej. Poniżej znajduje się lista powszechnych wyjątków, które występują podczas pracy z rzeczami związanymi z rejestracjami RMI, wraz z krótkimi wyjaśnieniami i zaleceniami naprawczymi. Zauważ, że każdy przypadek jest inny, więc nie zawsze zadziała w 100% przypadków.

Wyjątki po stronie klienta:

  • java.security.AccessControlException: access denied (java.net.SocketPermission hostname.server.com)

JVM nie ma uprawnień do otwierania zdalnych socketów. To może być problem z twoją polityką bezpieczeństwa Java (java.policy).

  • exception: Connection refused to host: 10.10.4.1; nested exception is:
     java.net.ConnectException: Connection refused

Połączenie nie może zostać nawiązane. Wynika to z tego, że rejestr nie jest osiągalny na poziomie sieciowym. Docelowy rejestr może być wyłączony lub coś blokuje połączenie sieciowe.

  • java.rmi.NotBoundException:

To oznacza, że rejestr istnieje, ale szukane przez ciebie powiązanie (bind) nie istnieje. Na przykład chciałeś powiązać się z „MYRegistry”, ale wkradła się literówka i napisałeś „MYRegistr”, dlatego otrzymałeś wyjątek NotBoundException.

  • exception: error unmarshalling return; nested exception is:
     java.lang.ClassNotFoundException: [ClassName]

To oznacza, że brakuje [ClassName] na twojej ścieżce klas. Spróbuj uruchomić klienta rmi (lub narzędzie) z katalogu, w którym znajduje się twoje interfejs. Jeśli uruchamiasz narzędzie w postaci pliku JAR, może być konieczne jego rozpakowanie, dodanie skompilowanej klasy do odpowiedniego pakietu (package) i ponowne spakowanie pliku JAR. Jeśli nie jesteś pewien, jak to zrobić, sprawdź poniższy link z wyjaśnieniem: https://www.geeksforgeeks.org/packages-in-java/

Exceptions when running the server:

  • java.rmi.server.ExportException: Port already in use: 1099; nested exception is:

Taki błąd występuje zazwyczaj, gdy próbujesz uruchomić rmiregistry dwukrotnie na tym samym porcie.

  • java.rmi.AccessException: Registry.Registry.rebind disallowed; origin foreign.host.com is non-local host

RMI może być powiązane tylko z lokalnym hostem – jeśli otrzymujesz ten błąd, prawdopodobnie próbowałeś powiązać się z registry, który znajduje się na zdalnej maszynie.

  • java.rmi.AlreadyBoundException: MyRegistry

To po prostu oznacza, że takie powiązanie już istnieje. Zmień nazwę lub odłącz poprzedni obiekt (unbinding) aby to naprawić.

To prawie wszystko, co trzeba wiedzieć na temat podstaw rejestrów RMI. Informacje tu zawarte są dość obszerne, ale jeśli chcesz inteligentnie korzystać z narzędzi do automatycznych ataków i być bardziej skutecznym w testowaniu infrastruktury, warto się tego nauczyć. W następnej części pokażę Ci techniki wyliczania i ataków, które mogą się różnić w zależności od poziomu bezpieczeństwa Java i polityki bezpieczeństwa docelowego systemu.

Referencje:

[1] https://www.slideshare.net/NickBloor3/nicky-bloor-barmie-poking-javas-back-door-44con-2017

Dodatkowo, tekst był inspirowany lekturą z następujących artykułów (z którymi polecam się zapoznać):

Czy Twoja firma jest bezpieczna w sieci?

Dołącz do grona naszych zadowolonych klientów i zabezpiecz swoją firmę przed cyberzagrożeniami już dziś!

Zostaw nam swoje dane kontaktowe, a nasz zespół skontaktuje się z Tobą, aby omówić szczegóły i dopasować ofertę do Twoich potrzeb. Dbamy o pełną dyskrecję i poufność Twoich danych, dlatego możesz nam zaufać.