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

Struts2 Object Manipulation

25 marca 2015, 09:16 | Teksty | 0 komentarzy
Tagi: , ,

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.

struts_object_manipulation_1

1. Strona powitalna po przesłaniu formularza.

2. Strona powitalna wywołana z parametrem.

2. Strona powitalna wywołana z 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:

3. Błąd podczas próby przesłania wartości z XSS'em.

3. Błąd podczas próby przesłania wartości z XSS’em.

Jednak wywołanie strony powitalnej z odpowiednim parametrem:

4. XSS zakończony sukcesem.

4. XSS zakończony sukcesem.

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.

5. Przywitanie użytkownika.

5. Przywitanie użytkownika.

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:

6. Zastosowanie parametru do zmiany nazwy użytkownika.

6. Zastosowanie parametru do zmiany nazwy użytkownika.

Lub, co ciekawsze, zmieńmy użytkownika w administratora:

7. Zastosowanie parametru do zmiany nazwy użytkownika i podniesienie jego uprawnień.

7. Zastosowanie parametru do zmiany nazwy użytkownika i podniesienie jego uprawnień.

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:

  1. Metody prepare,
  2. Mapowanie parametrów (tutaj przypisywane są wartości parametrów z requesta),
  3. 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

Linki:

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



Komentarze

Odpowiedz