Содержание статьи
Авторы статьи: Александр Антух и Артем Баранов
Сегодня мы хотели бы рассказать тебе об одном из самых веселых типов уязвимостей, которые встречаются в современных приложениях, — ошибках логики. Прелесть их заключается в том, что при своей простоте они могут нести колоссальный ущерб вплоть до полного захвата сервера и, что немаловажно, с большим трудом определяются автоматизированными средствами.
Но чтобы не грузить тебя определениями, которые более-менее очевидны и которые можно найти на тех же OWASP и CWE, перейдем сразу к делу.
Классика жанра
Для начала рассмотрим «лабораторный пример» — представим себе, что мы совершаем покупки в магазине, в котором при оформлении заказа передаются три параметра: ID товара, количество товара (quantity) и цена (price). Другими словами, запрос будет выглядеть следующим образом:
http://store/buy.asp?itemID=57&quantity=2&price=200.00
В случае если цена никак не валидируется (хотя зачем вообще ее передавать таким способом?), атакующий имеет возможность подменить ее значение и купить пару пылесосов по цене в 1 доллар. «Ха, кто же так делает?» — спросишь ты и будешь неправ: уязвимости подобного рода (ошибки бизнес-логики) встречаются в трех из пяти реальных проектов крупных заказчиков, включая европейских, аудиты для которых мы регулярно проводим.
К той же серии неверного использования параметров, заложенных, вероятно, еще на стадии проектирования, относится и следующий баг:
http://cabinet/personal/reset_password.asp?email=attacker@mail.com
Как ты наверняка успел заметить, в данном случае атакующий имеет возможность восстановить произвольный пароль на свою почту, причем данный параметр был оставлен разработчиками как «пасхалка» с комментарием remove this. И это в крупной финансовой организации!
Twitter big fish
Для полноты картины: подобные фейлы встречаются и у монстров наподобие Twitter. В числе одной из уязвимостей, зарепорченных нами от лица @defconmoscow, был следующий запрос:
POST /ajax/send_product_email HTTP/1.1
Host: about.twitter.com
...
email=VICTIM_EMAIL&language=][&download_link=HERE_COMES_SPOOFED_LINK&device=][&atc_product_download=twt_atc_mpp&utm_source=][&utm_medium=][&utm_term=&utm_content=&utm_campaign=][&form_build_id=][&form_id=][
Поскольку урл в параметре download_link не проверялся, было возможно организовать рассылку от лица Твиттера с предложением скачать мобильное приложение по указанной злоумышленником ссылке. Профит!
Немного классификации
Помимо «проблемных параметров», мы выделили следующие разделы, каждый из которых содержит в себе описание и примеры уязвимостей:
- прямое обращение к функционалу;
- незащищенные страницы;
- проверки на стороне клиента;
- ошибки многопоточности.
Давай рассмотрим их по порядку.
Прямое обращение к функционалу
Одна из самых веселых ошибок авторизации/бизнес-логики — функционал, доступ к которому открывается только после авторизации, но который при этом доступен и без авторизации. Первый пример — крупнейшая австрийская страховая компания (здесь и далее названия компаний не указываются по NDA и морально-этическим соображениям).
Вероятно, разработчики их веб-приложения решили привлечь новых клиентов, интегрировав в него различного рода дополнительный функционал, доступный в социальных сетях и уже привычный для многих: поделиться новостью с друзьями или быстро авторизоваться через учетную запись социальной сети. Однако использование такого рода «плюшек» может оказаться совсем не столь безобидным, как это кажется. Ниже представлена реализация механизма авторизации на сайте этой компании, использующая для аутентификации пользователя учетную запись в Facebook:
public function loginUserFB()
{
$fbId = ($_POST['fb']);
if($this->authentication_model->checkLoginUserFB($fbId))
{
$result['success'] = true;
...
}
else
{
$result['success'] = false;
...
}
}
Как видно из кода, единственное, что требуется для авторизации на сайте, — это вызвать функцию loginUserFB с FB-токеном, который может быть получен любым, кто знает страницу жертвы на Facebook (например, с помощью findmyfacebookid.com). Другими словами, если мы знаем человека, зарегистрированного на данном сайте через его FB-аккаунт, мы можем получить полный доступ к его учетной записи без ввода каких-либо паролей или ответа на секретные вопросы.
Но это еще не все: так как не все пользователи авторизуются через Facebook, то в таблице со списком зарегистрированных аккаунтов вместо токенов у части пользователей будут проставлены нули.
Таким образом, передав вместо токена 0
, мы сразу же получаем доступ к первому по списку пользователю из таблицы USERS
. Аналог данной таблицы представлен на рис. 1.
В данном случае это была учетная запись администратора системы, имеющая доступ к панели администрирования сайта. Позже именно через эту панель с помощью формы загрузки изображений на сайт был успешно залит шелл и получен полный доступ к целевой машине.
Еще один пример из жизни — неавторизованный доступ к API. Следующий запрос позволял получить данные о транзакции пользователя (включая идентификатор, время, сумму и другую информацию) обычным GET-запросом: /api/transactions/[id]
.
Что забавнее, добавление/обновление пользовательских данных осуществляется стандартным PUT-запросом и по-прежнему без авторизации! Оставим читателю пространство для воображения, что можно сделать в этом случае :).
Еще один пример — приложение российских государственных структур. В данном случае логика разработчиков была направлена именно на обеспечение безопасности и надежности системы — они решили удалять старые или уже не используемые в веб-приложении картинки и аватарки пользователей для высвобождения места и предотвращения DoS-атак.
Однако по каким-то причинам функционал удаления был выделен в отдельную процедуру deletephoto, которая в качестве аргумента принимала путь к удаляемому файлу, да еще и с полными правами для любого, в том числе и незарегистрированного, пользователя!
Вот так выглядел GET-запрос для удаления аватарки пользователя с ID 1773: /edit/upload/index.php?mode=deletephoto&filename=/1773/profile739.jpg
Только этот факт позволил удалять с сервера веб-приложения фотографии и остальные данные, загружаемые другими пользователями, включая их аватарки и материалы, используемые в статьях и заметках.
Но это еще не все. Очень скоро выяснилось, что для параметра с путем к файлу не была реализована фильтрация входных данных на path traversal, то есть в качестве аргумента можно было передать путь к файлу, находящемуся в корневой директории:
/edit/upload/index.php?mode=deletephoto&filename=../../../index.php
/edit/upload/index.php?mode=deletephoto&filename=../../../.htaccess
В результате стало возможным удалить файлы index.html
и .htaccess
из корневой папки с веб-контентом, что позволило получить листинг всего содержимого этой директории.
Именно эта уязвимость в дальнейшем позволила найти в одной из поддиректорий файлы, содержащие персональные данные зарегистрированных там пользователей.
Незащищенные страницы
Еще один тип логических уязвимостей и уязвимостей авторизации — незащищенные страницы. Как и в случае с предыдущим разделом, зачастую статичные страницы, страницы без референсных ссылок и прочее, попадающее под описание information disclosure, часто может быть проэксплуатировано, что приведет к катастрофическим последствиям. Следующие два примера показывают, как подобные вещи, а именно невнимательность и непридание значения таким страницам позволили довести раскрытие информации до RCE. Первый пример — крупная корейская медицинская организация. Данные пользователя надежно защищены, на страницах минимум динамики, повышения привилегий не предвидится, в общем, на первый взгляд все печально. Единственное, что привлекает внимание, — при создании карточки пользователя есть функционал загрузки фотографий. Нет, залить шелл через него не удалось — как следует из комментариев на HTML-странице, это «улучшенная версия загрузчика». Оттуда же следует, что неулучшенная версия все еще доступна в папке photo_uploader. Неавторизованный доступ к ней, уязвимость при проверке расширения фото и пренебрежение принципом минимальных привилегий — и через шестьдесят секунд имеем рут.
Таким образом, один комментарий на странице данных авторизованного пользователя приводит к тому, что произвольный неавторизованный злоумышленник может за один вечер выгрузить все данные о пациентах. Boo! Более длинная цепочка уязвимостей, которая привела к схожему результату, была проведена нами в еще одном значимом для государства веб-приложении. Все началось с тестирования функционала отправки фидбэков...
Любой, даже незарегистрированный пользователь мог отправить фидбэк модератору сайта. Сообщение отправлялось через POST-запрос (рис. 5), в котором помимо тела сообщения, имени и email отправителя можно было передать и файл.
Но в ответ возвращалось лишь сухое «Спасибо! Ваше сообщение отправлено». Это не просто не позволяло узнать, куда и с каким именем этот файл был сохранен, но даже наводило на мысль, что все сообщение — это обычное письмо с аттачем, отправляемое на почту модератору. Потому особого профита в этой форме мы не усмотрели и продолжили тестировать остальной функционал.
На том же сайте работала система сбора статистики и мониторинга Piwik (рис. 6). Большинство настроек в ней были выставлены по умолчанию, включая активный гостевой аккаунт, который позволял любому пользователю сайта получить подробную статистику о том, кто, когда и какие именно страницы сайта посещал. Конечно, данные об IP-адресах и датах посещения пользователями сайта интересны, но самым ценным здесь был список посещенных другими пользователями внутренних страниц сайта, включая раздел модератора. Несмотря на отсутствие доступа к этому разделу, в паре страниц, вероятно добавленных уже после передачи сайта в эксплуатацию, данная проверка все же отсутствовала, и, как результат, они и весь функционал на них были доступны любому, кто знал об их существовании.
Именно там, в форме поиска пользователя по имени, нами и была найдена SQL-инъекция, от которой был очень хорошо защищен весь пользовательский функционал по умолчанию. Поскольку права внутри БД были лимитированы и работать с файловой системой через эту инъекцию было нельзя, мы сдампили всю базу и начали изучать ее на факт наличия каких-либо конфигурационных данных, паролей или другой информации, позволявшей продолжить атаку и захватить весь сервер. Тут и была найдена таблица feedback, содержащая все отправленные нами сообщения, имена пользователей, отправивших их, и… имена к файлам, которые все же сохранялись в директории с веб-контентом, да еще и с тем же расширением, которое было указано в поле filename POST-запроса (рис. 7)!
Каких-то пара минут, и однострочный шелл уже на сервере в файле, хоть и с рандомным именем, но с заветным расширением php:
/common/upload/feedback/778eb7b67ad52fd74f581a29f1e9b48d.php
Уверен, дорогой читатель, тебе не составит труда понять, что за команда была выполнена и какой результат мы получили на рис. 8.
Проверки на стороне клиента
Один из самых частых паттернов логических уязвимостей — проверки на стороне клиента. Другими словами, фильтры на клиентской стороне не позволяют ввести/изменить определенные данные, но при этом сервер не проводит никакой дополнительной валидации. Классический пример — нередактируемые поля в банковских переводах. Рис. 9 отображает результат успешного обхода таких проверок: в одном случае (банк top-3 Австрии) существовала возможность создания транзакций, когда у лимитированного пользователя такая возможность не подразумевалась, в другом (банк top-10 России) — возможность изменения нередактируемых полей и подмены номера счета в переводах.
Тем не менее это тоже не все — одна известная в Европе страховая организация решила использовать в качестве основной защиты критически важного функционала JavaScript. Речь идет не об XSS (хотя куда уж без них), но о доступе к админке. Если неавторизованный пользователь пытается обратиться к защищенной админской странице /manage/environment/administrator/, скрипт проверяет валидность таких намерений и либо пропускает пользователя, либо отправляет на страницу логина. Честно говоря, поскольку использование NoScript у меня стоит по умолчанию, я был весьма удивлен, увидев таблицу с данными пользователей, и лишь после просмотра кода страницы обнаружил эту чудесную фичу. Если упростить, проверка валидности обращения к странице выглядит следующим образом:
<script language=javascript>
sure = confirm('Are you admin?');
if (sure)
location.href='/manage/index.jsp';
else
history.back();
</script>
Ошибки многопоточности
Наконец, еще один тип ошибок логики из нашего рейтинга — ошибки более низкого уровня абстракции — состояние гонки (Race Condition). Состояние гонки — «ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода» (с). Другими словами, если, например, два потока одновременно получают доступ к данным на запись, результат может быть непредсказуемым. Так и случилось с одной крупной британской платежкой — для получения доступа к данным невезучего пользователя понадобилось... обновить страницу! Начав разбираться, мы поняли, что проблема в механизме генерации сессии — при неудачной авторизации и определенном стечении обстоятельств сессия пользователя выставлялась в null и не менялась при следующем успешном логине. Соответственно, написав простенький скрипт, который обращался к главной странице с данными пользователя с нулевой сессией, мы за один вечер сграббили восемь аккаунтов с суммарным счетом более 50K долларов. Более подробно об этом ты можешь прочитать в презентации.
Summary
Как ты видишь, приведенные примеры не являются чем-то запредельно сложным — достаточно просто поверить в закон вселенского фейла :). При этом критичность найденных уязвимостей позволяла в ряде случаев залить шелл и получить полный контроль над приложением/сервером и всегда несла в себе ощутимый финансовый и репутационный ущерб для организации. Дерзай! Но соблюдай закон!