Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Struts2 Object Manipulation
Wstęp
Witam ponownie! Opisany w poprzednim tekście Zero-Day nie jest wynikiem błędnego działania frameworku, jest wynikiem tego w jaki sposób on działa.
Strutsy dają programiście bardzo wygodny sposób na operowanie atrybutami na linii frontend <-> backend aplikacji. Dają możliwość przypisania URI do metod w klasach zwanych akcjami. Framework na podstawie pliku konfiguracyjnego wywołuje odpowiednie akcje, gdy użytkownik odwiedza przypisane do nich URI. Jeśli klasa akcji posiada atrybuty, które mają wpływać na wyświetlaną stronę (wartości wpisane przez użytkownika w formularzu itd.), klasa akcji musi posiadać metody dostępowe:
- getNazwaAtrybutu – daje możliwość odczytywania wartości atrybutu,
- setNazwaAtrybutu – daje możliwość modyfikowania wartości atrybutu.
Jeśli developer chce przypisać pole formularza do atrybutu, stosuje jego nazwę jako „name” pola. Po wysłaniu formularza odpowiednie metody odszukają i próbują uruchomić metodę modyfikującą atrybut. Stosując zapytanie GET, można osiągnąć to samo (tzn. zmienić wartość atrybutu). Można tego dokonać dodając parametry do URI. Przykładem niech będzie typowa aplikacja HelloWorld, która pyta o imię użytkownika, a potem je wyświetla w powitaniu:
Kod strony powitalnej:
<h1>Witaj <s:property value="inputName" escape="false"/></h1> <s:a action="changeName">Zmiana imienia</s:a>
Kod strony zmiany imienia:
<h1>Zmiana imienia</h1> <s:actionerror cssStyle="color: red;"/> <s:form action="updateName"> <s:textfield name="inputName" label="Nowe imię"/> <s:submit/> </s:form>
Zaprezentowany kod leży po stronie prezentacji (frontend), kod backendu zaprezentuję później. Wynikowy formularz w HTML prezentuje się następująco:
<h1>Zmiana imienia</h1> <form id="updateName" name="updateName" action="/struts-empty/updateName.action" method="post"> <table class="wwFormTable"> <tr> <td class="tdLabel"> <label for="updateName_inputName" class="label">Nowe imię:</label> </td> <td> <input type="text" name="inputName" value="nieznajomy" id="updateName_inputName"/> </td> </tr> <tr> <td colspan="2"> <div align="right"> <input type="submit" id="updateName_0" value="Submit"/> </div> </td> </tr> </table> </form>
Łatwo z niego wywnioskować, że atrybut po stronie akcji nazywa się „inputName”. Poniżej wynik wysłania formularza oraz bezpośredniego wywołania strony z odpowiednim parametrem.
Jak te możliwości mają się do atakowania web aplikacji?
Tak jak wspomniałem wcześniej każda akcja ma przypisany adres, który powoduje jej wywołanie. Najczęściej inna metoda jest wywoływana podczas przygotowywania i wyświetlania formularza, a inna podczas obsługi wysłanego formularza, np.:
1. Wyświetlenie formularza pytającego o imię
/* * przygotowanie formularza */ public String changeName() { return ActionSupport.SUCCESS; } @Override public void prepare() throws Exception { if (inputName == null || inputName.length() == 0) { inputName = (String) getHttpSession().get(NAME_FLAG); if (inputName == null || inputName.length() == 0) { inputName = "nieznajomy"; } } }
2. Ustawienie imienia
/* * obsługa formularza */ private static final Pattern patternLetters = Pattern.compile("[a-zA-ZążźśęćńłóĄŻŹŚĘĆŃÓŁ]{3,}"); public String updateName() { if (inputName != null) { if (patternLetters.matcher(inputName).matches()) { getSession().put(NAME_FLAG, inputName); return ActionSupport.SUCCESS; } } System.out.println("Niepoprawna nazwa!!"); addActionError("Niepoprawna nazwa"); return ActionSupport.ERROR; } public String getInputName() { return inputName; } public void setInputName(String inputName) { this.inputName = inputName; }
3. Wyświetlenie powitania
/* * wyświetlenie powitania */ @Override public String execute() throws Exception { return ActionSupport.SUCCESS; }
Wszystkie metody znajdują się w tej samej klasie, tak więc podczas odwołania do każdej z nich możliwy jest dostęp do atrybutu przechowującego nazwę użytkownika. Próba wykonania XSS-a na formularzu zakończy się błędem podczas przetwarzania:
Jednak wywołanie strony powitalnej z odpowiednim parametrem:
zakończy się sukcesem.
Jak widać atak się powiódł. Wartość atrybutu została zmieniona poza metodą obsługującą przesłany formularz. Gdyby sprawdzanie nowej wartości znajdowało się w metodzie setInputName()
, wartość zostałaby odrzucona.
Atak na ClassLoader nie był atakiem na atrybut znajdujący się w klasie akcji, tylko na obiekt ClassLoadera, do którego klasa akcji ma dostęp. Wykonanie tego ataku było możliwe dzięki możliwości dostępu do atrybutów obiektów znajdujących się w akcji. Takie podejście pozwala, z jednej strony np. w łatwy sposób operować na obiektach bez potrzeby tworzenia specjalnych interfejsów. Z drugiej strony, naraża na ataki z nieautoryzowaną modyfikacją wartości atrybutów obiektu. Dla przykładu stwórzmy klasę przechowującą obiekt użytkownika:
public class User { private String name; private String password; private boolean admin; public User(String name, String password, boolean admin) { this.name = name; this.password = password; this.admin = admin; } public boolean isAdmin() { return admin; } public void setAdmin(boolean admin) { this.admin = admin; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
Jak widać obiekt użytkownika będzie przechowywał jego nazwę, hasło i flagę określającą, czy dany użytkownik jest administratorem. Po zalogowaniu się aplikacja sprawdza, czy użytkownik ma uprawnienia administracyjne i wyświetla inne powitanie:
<h1>Witaj <s:property value="user.name"/>!</h1> <s:if test="user.admin"> Jesteś administratorem! </s:if> <s:else> Jesteś użytkownikiem! </s:else>
Klasa akcji posiada jedynie funkcję umożliwiającą odczyt obiektu przechowującego dane użytkownika, pozwala to w łatwy sposób odnosić się do atrybutów obiektu i np. wyświetlać je na stronie.
Funkcja getUser zwraca cały obiekt użytkownika, dając dostęp do metod, które posiada, w tym metod umożliwiających zmianę atrybutów. Zobaczmy, co się stanie, gdy dodamy parametr z naszą propozycją nazwy użytkownika:
Lub, co ciekawsze, zmieńmy użytkownika w administratora:
Jak widać na powyższych przykładach, znając atrybuty w używanych obiektach, można namieszać w aplikacji. Przykładowo można wstrzyknąć XSS-a bądź kod SQL. Użytkownik może zmienić poziom swoich uprawnień, bądź podszyć się pod innego użytkownika (zmieniając identyfikator swojego obiektu). Oczywiście taka modyfikacja atrybutów nie zawsze jest możliwa. Wszystko zależy od tego, w którym momencie przygotowywane są obiekty (np. zaczytywane z bazy danych). Poniżej lista etapów, na których obiekt jest modyfikowany:
- Metody prepare,
- Mapowanie parametrów (tutaj przypisywane są wartości parametrów z requesta),
- Wywołanie metody akcji.
Jeżeli obiekt jest zaczytywany wewnątrz metody obsługującej request (pkt. 3), wszelkie zmiany wprowadzone w poprzednich etapach zostaną nadpisane i nie będzie możliwe manipulowanie atrybutami. Jeżeli developer skorzysta w klasie akcji z interfejsu Preparable i będzie przygotowywał wszystkie obiekty w metodzie „prepare” (pkt. 1), wartości przekazane w requeście nadpiszą te przygotowane przez metodę (możliwa będzie manipulacja obiektami i atrybutami).
Warto dodać, że poprzez URI można dostać się bezpośrednio do obiektu request i sesji, np. session.flaga=cokolwiek, ustawi w sesji atrybut „flaga”. Jest tylko jedno ALE… parametry ze słowem session.*, tak samo jak kilka innych (chociażby class i classloader, które wywołały całe zamieszanie) zostały dodane do listy parametrów zakazanych, których aplikacja nie przetworzy. Oczywiście zawsze może się okazać, że programista stworzył w akcji metodę, która zwraca sesję i dał jej publiczny dostęp, tym samym dając do niej dostęp potencjalnym atakującym (jednak tutaj wymagana jest znajomość struktury aplikacji i nazwy metody).
Podsumowanie
Opisany tu atak pokazuje, że frameworki ułatwiają życie nie tylko twórcom oprogramowania. Dokładna analiza tego jak dany framework działa może często otworzyć furtkę do aplikacji, która wydawała się solidnie zabezpieczona. W przypadku Struts, pełna kontrola nad wszystkimi obiektami jest trudna do zrealizowania (wystarczy spojrzeć na różne kawałki aplikacji dostępne online, aby zrozumieć, jak popularne jest stosowanie łatwego dostępu do obiektów).
Marcin Gębarowski– marcing.dev[at]gmail.com