ASP.NET Core: Механизмы предотвращения атак 2.0

По встроенным механизмам безопасности ASP .NET Core написано мало статей. Даже официальная документация имеет пробелы. В этой статье мы пройдём по всем основным компонентам, имеющим отношение к безопасности, и разберём, как это работает внутри.


Если вы используете старый добрый ASP .NET, то для вас будет полезна информация по внутреннему устройству компонентов безопасности и лучшим практикам их использования. Здесь вы найдёте ответы на следующие вопросы: как реализованы современные анти-XSS механизмы и как их правильно использовать в ASP .NET Core? Как правильно работать с cookies и какие подводные камни там могут встретиться? Как был переписан механизм защиты от CSRF? Как правильно работать с криптографическими алгоритмами? Кроме того, рассказывается про опыт участия в Bug Bounty по поиску уязвимостей в ASP .NET Core.


Перед чтением рекомендуется освежить в памяти атаки из списка OWASP Top 10.


Прототипом статьи является доклад Михаила Щербакова на конференции DotNext 2017 Moscow. Михил — Microsoft .NET MVP, участник .NET Core Bug Bounty Program, соорганизатор сообщества .NET программистов (Московское комьюнити называется MskDotNet, питерское — SpbDotNet). По работе последние 5 лет занимается безопасностью. Работал в Positive Technologies, в Cezurity, сейчас как консультант работает напрямую с заказчиками, по большей части в этой же сфере. Профессиональные интересы: статический и динамический анализ кода, информационная безопасность, автоматизация отладки кода, исследование внутреннего устройства .NET CLR.


В этом тексте огромное количество картинок со слайдов. Осторожно, трафик!



trnz6khjtpcotehqohvasahbxvo.png


В этой статье мы поговорим про атаки и механизмы защиты, а именно про Open Redirect, про изменения в Crypto API в .NET Core, про XSS и Client-side атаки, про настройку CSP, CSRF, CORS и вообще про правильные паттерны использования cookies.


gytt8ulyj9m4v3qc2ydiby5av6i.png


У Microsoft сейчас открыта бессрочная Bug Bounty программа, и я в ней участвую. Если кто-то из вас интересуется и занимается безопасностью, вы можете поискать уязвимости в .NET-платформе, отправить им report. У них приятное вознаграждение, где-то в среднем 5- 10 тысяч они готовы платить за найденную верифицированную уязвимость. И это приятно, что Microsoft сейчас вкладывает довольно большие усилия в безопасность .NET-платформы и .NET Core в частности.


trl6-o3cvt-2a1vipu3frtmujje.png


Начнем с предотвращения атаки Open Redirect. Я думаю, многие про него знают, но не знают, что именно это называется Open Redirect. Давайте посмотрим на демку. Я думаю, большинство из вас сходу поймут, в чём заключается атака.


rw9chmaagmc13jaiurpqiaoqvo8.png


Итак, у нас есть некий сайт, который написан на ASP.NET, он называется my-telegram.com, у нас есть некий злоумышленник, который составляет следующий url на форму логина. Это совершенно стандартная ASP.NET форма логина. Атакующий отправляет эту ссылку жертве и каким-то образом заставляет по этой ссылке перейти.


rd56j7qxqssfjmv3bghb_zjzony.png


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


ptb0on-k5bxbqne_xq7c9psvnn0.png


«Что-то пошло не так, введите заново пароль и логин». Обычная реакция — «наверное, я где-то неправильно что-то ввел», и пользователь повторяет ввод. Но если вы обратили внимание, в верхней строчке уже не my-telegram, а my-telergam, небольшая игра с символами. Что произошло? Первая форма логина на доверенном сайте совершенно нормально залогинила пользователя на этом сайте, дальше был редирект. А в редиректе был url вот на этот сайт. Если мы не проверяем url в редиректе, то он вполне себе валидно отправляет нас на другой сайт, где уже с помощью некой социальной инженерии заставляет нас повторно ввести логин и пароль, которые уходят на сайт атакующего, и дальше атакующий уже может с этим работать.


c9phihxazs9dcb2u7oofswh2jzi.png


Как это предотвратить? У ASP.NET в контроллере есть метод, LocalRedirect, я думаю, все его видели, он как раз защищает от таких случаев, проверяет, что у вас не абсолютный путь, что он относительный и может редиректить только на этот сайт.


z2tzrg0s0hi9m-5oo_m9298qxgg.png


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


owmtfdegohuhi14k5_4hx8aw7y8.png


В этом году OWASP собирал свою статистику, по ссылке на GitHub можно посмотреть. При всей простоте защиты, Open Redirect стоит на третьем месте по популярности в мире.


pcnixcn1wbvqp97lbb5eymplgdi.png


С этой же атаки началось мое участие в Bug Bounty. Мой первый репорт, который был пофиксен и оплачен, был как раз про обход встроенного механизма Open redirect. Но сейчас там все нормально, он пофиксен. LocalRedirect действительно защищает на 100% от возможного редиректа на сторонний сайт.


Следующее, про что хотелось бы поговорить, — это Data Protection. В Crypto API были огромные изменения в .NET Core. По сути, всё API было полностью переписано, поэтому давайте быстро пройдемся по основным моментам и посмотрим, что поменялось.


xmmuo0mb9eimtaacblozyxaq0ow.png


Во-первых, отказались от использования Machine Keys. Machine Keys раньше являлся master key для всей криптографии. С этим было много проблем, особенно в распределенной системе, вам нужно было сочинять какое-то свое хранилище ключей, чтобы то, что зашифровала одна нода, было возможно расшифровать другой нодой. Также не было нормальной возможности отозвать, например, зашифрованные cookies. Вам нужно было бы менять Machine Keys. Было очень много всяких неудобств. Сейчас от этого отказались, сделали нормальные высокоуровневые API из коробки. Раньше System.Security.Cryptography представляла из себя некий набор инструментов, используя которые очень легко было сделать ошибку.


Частый правильный кейс — использовать некую стороннюю библиотеку, которая уже предоставляет высокоуровневые API, и вам не нужно заботиться обо всех нюансах. Сейчас это есть в .NET Core из коробки. Есть хранилище ключей из коробки: вы можете сконфигурировать какое-то удаленное хранилище, где у вас будут лежать все ключики, и все ноды будут туда за ними ходить. Оно легко расширяется, вы можете использовать как уже существующие в Redis или в Azure, так и написать своё. Поддерживается ротация ключей, раз в 90 дней ключик меняется, и для создания нового шифротекста уже будет использоваться новый ключ, а старый — только для расшифровки старого. Также предоставляется изоляция подсистем из коробки. Можно кастомизировать, то есть использовать свои кастомные алгоритмы шифрования, чуть позже я покажу пример.


Итак, как выглядит новый Crypto API?


ycyldflmkgwfvzrabrcbbytqm_k.png


Довольно-таки примитивно, легко, так как у нас есть DI в .NET Core, вы можете просто в конструктор контроллера передать IDataProtectionProvider, создать из него протектор и использовать метод Protect и Unprotect для шифровки и расшифровки. Всё, больше вам ни о чем заботиться не нужно. И еще один параметр, на котором я ещё хотел остановиться, — это как раз та самая изоляция на уровне протекторов, где каждый протектор с разными строковыми идентификаторами может расшифровывать данные, которые только он зашифровал. Если вы хотите шифровать данные для разных пользователей и быть уверенным, что они не смогут чьи-то чужие зашифрованные данные расшифровать в своём аккаунте, то это удобная фича, которая доступна из коробки.


Есть еще такая вот вишенка:


bpgzfzg_tqogtkxzqxvk0ucy9jo.png


Вы можете использовать time-limited протектор для создания токенов. Когда вам нужно определить время жизни вашего ключа, например, токена, вы просто его шифруете time-limited протектором, определяете его время жизни, и при расшифровке, если это время истекло, вы просто получите эксепшн.
Следующий момент — хранение паролей. Как все знают, в открытом виде мы пароли не храним никогда, вопрос, как сделать это правильно. Опять же, у нас есть механизм в .NET Core, паттерн для правильного создания хэшей:


fdwspxrfmnj-w570f4zb92lsziq.png


Во-первых, вы используете для создания соли рандомный генератор из Crypto API. Созданная им соль передается вместе с ключом в метод, который, используя заданную хэширующую функцию, создает вам хэш. Этот хэш вместе с солью вы уже спокойно сохраняйте в базу данных и уже используете эту пару дальше для верификации введенных пользователем паролей.


2bmrbwgqnvnoyjxe0wzhljign_c.png


Все это конфигурится стандартным методом в ConfigureServices. Вы можете задавать уровень изоляции для всего приложения, чтобы разные приложения не могли расшифровывать данные друг другу, если это необходимо. Можете устанавливать время жизни мастер-ключа, можете настроить, где у вас будут храниться ключи.


fpqswgnzzfgsf6cirahamc1scuk.png


Можете кастомизировать алгоритмы шифрования, которые вы хотите использовать. По умолчанию будет использована AES-256 с дополнением CBC. Это блочные алгоритмы шифрования, и без дополнительной подписи они уязвимы к атакам Padding Oracle. Эта проблема была как раз в .NET в большом фреймворке из-за неправильного использования System.Security.Cryptography. Здесь же из коробки шифротекст будет подписан еще хэшом, и в этой ситуации Padding Oracle невозможен, так что можете об этом не думать. Есть еще нюанс: каждый шифротекст будет иметь заголовок, который состоит из 32-битного magic header, следом за которым идет айдишник ключа. Это иногда бывает полезно для отладки, когда вы хотите понять, каким ключом был зашифрован текст. Вы просто смотрите на первые 20 байт и видите, одинаковыми или разными ключами они зашифрованы в зависимости от того, одинаковые или разные эти хедеры.


lm-6dbyz7bpaeonk6vrzyu0i3be.png


Перейдем к большой части, посвященной Client-side уязвимостям. Начнем с XSS. Все знают, что такое XSS. Но всё равно начнем с самых азов, потому что понимание, как работает в браузере Same-origin policy (SOP) очень важно для полного понимания атак на Client-side. Рассмотрим вот такой кейс:


x-fljnjoq3n4007brlvxlxej3ku.png


У нас есть браузер, в одной вкладке открыт сайт foo-example с JavaScript-кодом, который выполняет GET-запрос на другой домен. Есть другой сайт по этому домену bar.other, который у нас возвращает Json на этот запрос, какие-то цены на продукты. И вот вопрос — отправит ли браузер этот запрос? В этом случае выглядит логично, что хорошо бы отправить этот запрос и получить ответ. Какая проблема с этим есть?


_kaqypovevp7pnydep9njaqiyl4.png


Если в нашем примере мы просто поменяем сайты, и вместо провайдера данных у нас какой-то mail.google, а здесь у нас какой-то сайт атакующего, который атакующий заставил нас открыть в своем браузере, то если бы этот запрос был отправлен и ответ получен, атакующий бы получил данные из нашего почтовый ящика, что плохо. Поэтому и существует Same-origin policy, который предотвращает чтение данных в подобном случае. А правильный ответ на мой изначальный вопрос, отправит ли браузер запрос — да, отправит. Браузер отправляет POST/GET-запросы в этой ситуации, но не позволяет JavaScript прочитать ответ этого запроса, поэтому утечки данных здесь произойти не может из-за Same-origin policy. Но на самом деле запросы будут отправлены, это будет важно для понимания других Client-side атак.


bg2jgusx9dmgd9rf04q6blhzapa.png


Что же такое вообще Same-origin policy? Это некие ограничения, которые накладываются на загруженный документ или скрипт из одного источника на взаимодействие с ресурсами из другого источника.


65dydogxzyyf7dppsn1jwqjrkbi.png


Что же такое источник, что такое origin? Origin представляет из себя набор схемы http, https либо какой-то другой, домена и порта. То есть скрипт, выполняющийся на одном домене, на одном Origin с портом и схемой, не может получить данные с другого Origin без каких-то дополнительных действий. Как атакующий может всё-таки получить данные, какие атаки на Client-side существуют?


Первая — это XSS, с которой мы начали. Суть атаки — если мы не можем с другого Origin получить данные, то давайте наш скрипт заинжектим прямо в Origin сайта, сделаем инъекцию нашего вредоносного скрипта на доверенную веб-страницу, и тогда из Origin мы уже можем делать запрос внутри этого Origin и получать данные страницы. Это суть XSS.


wvxtchsubsusxn3rigtxpnfddoy.png


При всей известности и простоте это остается самой популярной атакой в мире. Она является очень хорошей точкой входа для других атак. Существует много атак, которые можно развернуть, начиная с XSS. Она является общеизвестной, при этом очень много проектов, где разработчики недостаточно используют встроенные средства защиты от XSS. И вообще не придают ей серьезное значение.
Давайте посмотрим, что у нас есть в ASP.NET, чтобы правильно построить защиту от этого вида атак. Возьмем два таких синтетических примера варианта XSS:


v3kxg4d6wszwbnfxhlvlrmcbz5c.png


Первый вариант: у нас есть некий скрипт, у него есть source, в source вставлен какой-то путь. Если атакующий может манипулировать этим путем (загрузить свой source) или заинжектить всю эту строчку в документ, соответственно, он заинжектит свой скрипт. Это и есть XSS-атака.


fgrmgxo7qaje4ismjfepq4nnwia.png


Второй вариант — когда у нас фигурирует не параметр скрипта, а атакующий может в какой-то тег html заинжектить свой код, например, напрямую в