Продолжаем цикл статьей «Календарь тестировщика», в этом месяце поговорим о тестировании безопасности. Многие не знают с чего начать и пугаются сложностей. Иван Румак, тестировщик безопасности веб-приложений в Контуре, поделился основами в поиске уязвимостей. Новички найдут в статье базовые знания, а опытным тестировщикам будет полезен раздел про обход защиты от CSRF.
В прошлом году Иван занял 4 место в программе поиска уязвимостей Mail.ru и вошел в призовые топ-100 соревнования Hack The World 2017.
В феврале я решил научить коллег-тестировщиков искать уязвимости и проверять релизы на баги безопасности. Из плана обучения я вынес в статью самые основы: с чего начать, что такое HTTP, а также сделал полный разбор одной уязвимости — как искать, защищаться и обходить защиту.
![](https://habrastorage.org/webt/i8/e_/ze/i8e_ze4s-6pjxvy5wgjm--_ukyc.jpeg)
С чего начать?
С этой проблемой сталкиваются многие новички. Кто-то первым делом идет на 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¶m2=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¶m2=value2
— text/plain
Content-Type: text/plain
anytext{"param":123}><<>><xml>
Параметры, передаваемые на сервер. Содержат имя и значение. Записываются либо после URI как ?param=value¶m2=value2¶m3=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.
![](https://habrastorage.org/webt/xc/si/ro/xcsirov_ezlegkuy2v4rzq28eis.png)
Если сервер принял такой запрос от csrftest.html как обычный и что-то изменил, то можно заводить баг.
Последовательность работы:
- Прокликивай приложение и лови запросы в консоли браузера или отладочной проксе.
- Если что-то изменилось через GET-запрос, попробуй повторить его с html страницы как <img src=”весь путь с параметрами”>.
- Если изменения произошли через POST-запрос и защиты от CSRF нет, повторить его в теге form.
- Если есть защита, попробуй ее обойти.
Защита от 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”>
![](https://habrastorage.org/webt/ot/u4/8l/otu48lo3zblfsp0bxnvezlw8wn0.png)
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.
Что дальше? Изучай новые виды уязвимостей, ищи их у себя в проекте, устраняй их. Вместе мы сделаем интернет более безопасным!
Полезные ссылки
- База публичных баг-репортов по безопасности с сайта hacerkone. Можно поискать репорты по слову CSRF и увидеть, что находят у разных крупных компаний
- Канал в телеграме по теме безопасности
Список статей календаря:
Попробуй другой подход
Разумное парное тестирование
Обратная связь: как это бывает
Оптимизируй тесты
Прочти книгу
Тестирование аналитики
Тестировщик должен поймать баг, прочитать Канера и организовать движуху
Нагрузи сервис
Метрики на службе у QA
Протестируй безопасность
Узнай своего клиента
Разбери бэклог