«Календарь тестировщика». Протестируй безопасность

    Продолжаем цикл статьей «Календарь тестировщика», в этом месяце поговорим о тестировании безопасности. Многие не знают с чего начать и пугаются сложностей. Иван Румак, тестировщик безопасности веб-приложений в Контуре, поделился основами в поиске уязвимостей. Новички найдут в статье базовые знания, а опытным тестировщикам будет полезен раздел про обход защиты от CSRF.

    В прошлом году Иван занял 4 место в программе поиска уязвимостей Mail.ru и вошел в призовые топ-100 соревнования Hack The World 2017.

    В феврале я решил научить коллег-тестировщиков искать уязвимости и проверять релизы на баги безопасности. Из плана обучения я вынес в статью самые основы: с чего начать, что такое HTTP, а также сделал полный разбор одной уязвимости — как искать, защищаться и обходить защиту.




    С чего начать?


    С этой проблемой сталкиваются многие новички. Кто-то первым делом идет на OWASP — сообщество по безопасности веб-приложений. Самое полезное, что я вынес с OWASP — список наиболее опасных и распространенных уязвимостей веб-приложений. Осмысленное обучение началось, когда я начал детально изучать каждую из них и гуглить все незнакомые слова. Стало понятно, что изучать даже самые распространенные клиентские уязвимости (CSRF, XSS) крайне трудно без знания устройства протокола HTTP.


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


    Еще один отличный источник информации — чтение и воспроизведение публичных репортов других хакеров.


    Есть сайты, агрегирующие раскрытые сообщения о багах безопасности, например, http://h1.nobbd.de/. Там вы узнаете, какие бывают уязвимости, как их находят и чинят. Важно пытаться воспроизводить баги самому, чтобы получить практический опыт. Для этого можно использовать площадку для отработки поиска уязвимостей, например, DVWA.


    Про HTTP


    Суперважно знать, как устроено взаимодействие пользователя с веб-приложением. Поэтому расскажу про HTTP-протокол, через который клиент взаимодействует с веб-сервером. В нем нас интересуют:


    Методы. Для начала достаточно различать GET и POST. Указываются в первой строке запроса.


    GET — получить содержимое с сайта.


    GET / HTTP/1.1
    Host: example.com

    POST — что туда отправить.


    POST /endpoint HTTP/1.1
    Host: example.com
    User-Agent: Apache-HttpClient/4.5.5 (Java/1.8.0_161)
    Content-Type: application/x-www-form-urlencoded
    
    param1=value1&param2=value2

    URI: путь до файла или эндпоинта, указывается после слэша в первой строке запроса.


    Хэдеры — заголовки: User-Agent, Content-Type, Host и т.д. Бывают стандартные (Accept, User-Agent и т.д.) и кастомные с произвольными названиями и значениями (например X-Auth-Token: 123).


    Про Content-Type


    Content-Type нужно указывать для запросов, где параметры передаются в теле. Этот хэдер говорит веб-серверу, в каком формате отправляется содержимое запроса в теле. Основные 3 Content-Type:


    — application/json


    Content-Type: application/json
    
    {"param1":"value1","param2":"value2"}

    — application/x-www-form-urlencoded


    Content-Type: application/x-www-form-urlencoded
    
    param1=value1&param2=value2

    — text/plain


    Content-Type: text/plain
    
    anytext{"param":123}><<>><xml>

    Параметры, передаваемые на сервер. Содержат имя и значение. Записываются либо после URI как ?param=value&param2=value2&param3=value3, либо в теле в том формате, который указан в Content-Type.


    Cookie. Самый распространенный способ авторизации пользователя. Когда пользователь логинится в сервис, ему выдается уникальный ключ, который хранится в браузере и с которым он отправляет все дальнейшие HTTP-запросы к этому сервису. Используется для идентификации пользователей, чтобы клиент А не получил доступ к данным клиента Б.


    Когда пользователь нажимает кнопочки на UI, например, «Сохранить», он отправляет на веб-сервер такие HTTP-запросы, содержащие метод, URI, параметры, свои куки. Отловить отправленные вами запросы для исследования можно в браузере (в Хроме F12 -> Network), а отправлять через какие-нибудь API клиенты, например, Restlet Client. Либо воспользоваться отладочной прокси (Burp, Fiddler).


    Зная устройство HTTP-протокола, можно начинать изучение конкретных уязвимостей. В качестве примера я приведу уязвимость CSRF — с нее полезно начинать новичкам.


    Про уязвимость CSRF


    CSRF (Cross site request forgery) — возможность заставить пользователя отправить произвольный HTTP-запрос к уязвимому ресурсу. Уязвимость CSRF является «клиентской» — с ее помощью можно атаковать только других пользователей, но не сервер и внутреннюю инфраструктуру.


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

    Тег <form>. Этот тег в html-страничке отправляет GET или POST запросы к любому ресурсу.


    Пример:


    <form name=form1 action=”https://example.com/sendmoney” method=”POST”>
    <input type=hidden name=”amount” value=”9999”>
    </form>

    Атрибуты:
    name=”form1” — имя формы
    action=”https://example.com/test” — куда отправить запрос
    method=”POST”, method=”GET” — какой метод использовать
    enctype=”application/x-www-form-urlencoded” — с каким Content-Type отправить запрос. Если не указывать этот атрибут, по-умолчанию отправится как application/x-www-form-urlencoded.


    Чтобы указать параметры пользуйтесь тегом <input>.


    Его атрибуты:
    type=”hidden” — тип, почти всегда лучше использовать hidden. Если нужно загрузить файл, то использовать type=”file”, если нужна кнопка, которая отправит запрос в форме: type=”submit”.
    name=”sendmoney” — имя параметра
    value=”9999” — значение параметра


    Пример страницы:


    <html><body>
    <form name=form1 action=”https://example.com/changepassword” method=”POST”>
    <input type=hidden name=”newpassword” value=”123456”></form>
    <body onload=”document.form1.submit()”> <!-- отправляет form1 -->
    </body></html>

    При посещении такой страницы пользователь без своего ведома отправит примерно такой запрос:


    POST /changepassword HTTP/1.1
    Host: example.com
    Content-Length: 18
    Origin: https://evil.com
    Content-Type: application/x-www-form-urlencoded
    Accept: text/html, */*
    Cookie: auth.cookie.from.example.com=verysecret
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
    Referer: https://evil.com
    
    newpassword=123456

    Если на сервере нет проверки, откуда пришёл HTTP-запрос, то он обработает его как обычный. Т.е. пользователь сайта evil.com, отправит HTTP-запрос с кукой auth.cookie.from.example.com=verysecret, которую подставит браузер, и в контексте текущей сессии на example.com его пароль поменяется на 123456.


    Есть тонкости отправки запроса из HTML-страницы:


    1) Отправка запроса через тег form ограничивается только стандартными хэдерами. CSRF нельзя применить, если сессионный токен в приложении передается не через куки, а через авторизационный хэдер в каждом запросе, например, Authorization.


    GET /userdata HTTP/1.1
    Host: example.com
    Accept: text/html, */*
    Authorization: APIKEY123123123123123123
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
    Referer: https://evil.com

    2) Content-Type поддельного POST запроса может быть только application/x-www-form-urlencoded, multipart/form-data или text/plain. Опять же из-за ограничений тега form.


    3) Тег form позволяет отправлять только GET/POST запросы. PUT/PATCH/DELETE/MKCOL и прочие не пропускаются.


    Как искать CSRF


    Мониторить запросы, которые идут на сервер при работе в вашем приложении. Выбрать запросы на изменение чего-либо и попробовать отправить их через тег form из какого-нибудь файла csrftest.html.




    Если сервер принял такой запрос от csrftest.html как обычный и что-то изменил, то можно заводить баг.


    Последовательность работы:


    1. Прокликивай приложение и лови запросы в консоли браузера или отладочной проксе.
    2. Если что-то изменилось через GET-запрос, попробуй повторить его с html страницы как <img src=”весь путь с параметрами”>.
    3. Если изменения произошли через POST-запрос и защиты от CSRF нет, повторить его в теге form.
    4. Если есть защита, попробуй ее обойти.


    Защита от CSRF


    Чтобы защититься от непроизвольной отправки пользователем кросс-доменных запросов через тег form, убедись, что такой запрос приходит не с постороннего сайта.

    Как проверить, что запрос из формы пришел с вашего сайта?


    1) Каждый запрос, совершенный на сайте, передает уникальный токен в куках и в кастомном хэдере CSRFToken. Когда запрос получен, прежде чем что-то изменять этим запросом, проверяют совпадение значения хэдера с тем, что хранится в куках.


    POST /changepassword HTTP/1.1
    Host: example.com
    CSRFToken: dadfaae9-c625-4bdf-8804-c7977d96954f
    Cookie: session=123123123123; CSRFToken=dadfaae9-c625-4bdf-8804-c7977d96954f
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 61
    
    newpass=123456

    Минус такой защиты — для GET-запросов этот хэдер почти всегда необязателен. Если в приложении у какого-то эндпоинта, что-то изменяющего (напр. /changepass), можно перенести параметры из тела POSTа в урл и совершить запрос как GET (HEAD и OPTIONS, кстати, тоже могут так работать), при этом запрос отработает как полноценный POST, то такую защиту можно обойти вот так:


    <img src=”https://example.com/changepass?newpassword=123456”>




    2) То же самое, что и прошлый пункт, только токен в куках сравнивается с токеном в параметре.


    POST /changepassword HTTP/1.1
    Host: example.com
    Cookie: session=123123123123; CSRFToken=dadfaae9-c625-4bdf-8804-c7977d96954f
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 61
    
    newpass=123456&CSRFToken=dadfaae9-c625-4bdf-8804-c7977d96954f

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


    Можно попробовать обойти, если токен генерируется из двух частей: статической (например, хэш от айди пользователя) и динамической (хэш от даты получения токена). Тогда можно отправить токен только со статической частью. Или отправить запрос без токенов вообще, у Фейсбука был такой баг (https://amolnaik4.blogspot.ru/2012/08/facebook-csrf-worth-usd-5000.html).


    3) Content-Type каждого запроса должен быть отличен от поддерживаемых тегом form (urlencoded, text/plain, multipart/form-data).


    Это хороший способ защиты API от CSRF, если авторизация пользователя возможна и по кукам, и по кастомному хэдеру, и по параметру в урле.


    Если в корне сайта присутствует плохо настроенный crossdomain.xml, то через Flash можно заставить пользователя отправить запрос с любым Content-Type. Вот детальная статья про Flash.


    4) Same Site Cookie. Флаг на куках, который ставится при аутентификации вместе с httponly и не позволяет браузеру отправлять ваши куки с левых сайтов, к которому эти куки никак не относятся. Круто, но не все браузеры поддерживают этот флаг. То же самое про проверку Origin — классно, работает, но не все браузеры правильно посылают Origin при кросс-доменных запросах.


    Прямо сейчас проверь:


    — В твоем приложении у POST-запросов есть защита от CSRF.


    — Чувствительные действия (смена пароля, создание доп. пользователя в организации, отправка денег), которые отправляются как POST, в случае, если защита построена на токене в кастомном хэдере и токене в куках, не могут быть переделаны на GET с параметрами в урле из тела и что-то менять в системе (200 OK, 201 CREATED…). Аналогично с переводом PUT/PATCH в POST или GET.


    — Если чувствительные действия отправляются с “Content-Type: application/json” и защита от CSRF построена только на этом, то попробовать отправить запрос с телом в форматах application/x-www-form-urlencoded, multipart/form-data, text/plain. При успехе повторить с левого сайта в интернете через <form>.


    — В корневом каталоге сайта нет плохо настроенного crossdomain.xml, файла для кросс-доменных запросов с использованием Flash. (example.com/crossdomain.xml). «Плохо» — это когда любые кросс-доменные запросы могут быть отправлены с любого сайта на ваш, в crossdomain.xml явно описано, с какого домена и какие запросы можно совершать.


    Итог


    Теперь вы знаете про какие-то основы тестирования безопасности и где можно брать информацию для углубления в тему, можете проверять свои проекты на CSRF и разбираетесь в HTTP.


    Что дальше? Изучай новые виды уязвимостей, ищи их у себя в проекте, устраняй их. Вместе мы сделаем интернет более безопасным!


    Полезные ссылки


    Контур 102,60
    Делаем веб-сервисы для бизнеса
    Поделиться публикацией
    Комментарии 4
    • 0

      "2) Content-Type поддельного POST запроса может быть только application/x-www-form-urlencoded, multipart/form-data или text/plain. Опять же из-за ограничений тега form.


      3) Тег form позволяет отправлять только GET/POST запросы. PUT/PATCH/DELETE/MKCOL и прочие не пропускаются."


      что мешает использовать ajax? можно любой метод и content-type использовать

      • 0
        Кросс-доменные ajax-запросы фейлятся с ошибкой доступа, если нет плохо настроенного CORS (подставляет любой Origin из запроса в Allow-Access-Control-Origin) или других взаимодействий (postMessage от любого Origin, Flash со слабым crossdomain.xml, т.д.). Всё это отдельные баги, без которых кросс-доменные ajax-запросы невозможны.
        • +1
          Их «фейлит» браузер, не дает прочитать ответ js скрипту (он и не нужен), но сервер обрабатывает запрос как нормальный, если нет csrf защиты. Так что не стоит полагаться, что ajax защитит от csrf.
          • 0
            Можно совершать ajax-запросы без префлайта только для случаев, когда этот же сценарий мог быть совершен через тэг form. Для всего остального совершается префлайт и запрос зафейлится на нём, если нет плохо настренного CORS'а. Т.к. вектор этой атаки — браузер, то встроенная защита работает и защищает от CSRF.

            Примеры: GET with credentials — совершится, т.к. это то же самое, что подгрузка картинки с другого домена (находимся на example.com, где есть img src=«www.example2.com/favicon.ico» — в GET-запросе к картинке будут куки пользователя от example2.com)

            POST with credentials — совершаются, но только с Content-Type: text/plain, multipart/form-data, application/x-www-form-urlencoded. Если сделать POST with credentials с Content-Type: application/json, запрос зафейлится на префлайте.

            PUT, PATCH, DELETE… — требуют префлайт, то же самое.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое