Vavilen
10.07.2022, 20:25
CSRF на JSON API: от разведки до эксплуатации. Практический кейс
https://forum.antichat.xyz/attachments/4920546/img_23b4c6740f.png
ДИСКЛЕЙМЕР
Эта история — художественный вымысел. Все совпадения с реальными людьми или уязвимостями — случайность. Материал предоставлен исключительно в образовательных целях для специалистов по ИБ. Повторение подобных действий в реальной жизни приведет к серьезным проблемам с законом. Помните: вы не Вавилен.
Лирическое отступление
Вавилен был доволен своим рабочем местом в конторе. Он приносил деньги себе и своему начальству, которое давало для этого всю инфраструктуру. Но в один прекрасный день боссы, в очередной раз срубив куш на труде Вавилена, решили с ним не делиться. Так они потеряли ценного сотрудника и получили в ответ обманутого и очень мотивированного человека, жаждущего возмездия.
Постановка цели
После ухода из конторы все доступы к инфраструктуре, разумеется, были отозваны. Но это не проблема. Основные активы бизнеса крутятся в кастомном веб-приложении для обработки данных. Компрометация этого приложения — ключ ко всей нужной информации. В истории браузера, к счастью, остался IP-адрес сервера. С него и начнём.
Изучение цели
Чтобы не шуметь и не светить свой IP на боевом сервере, было принято очевидное решение — развернуть аналогичный софт у себя в лаборатории. ПО распространяется по подписке, но есть семидневный триал с предоставлением VPS. Идеально для наших тестов. Регистрируюсь через 10-минутную почту, разворачиваю софт, активирую триал. Можно приступать к вскрытию.
Первое, что нас встречает — стандартная форма логина.
https://forum.antichat.xyz/attachments/4920546/img_00cf84fdc3.png
Смотрю, как устроен механизм аутентификации. При попытке входа улетает
POST
запрос с телом в формате
application/json
. Ничего необычного.
Что там в JWT?
После успешного входа сервер в ответ ставит cookie с JWT:
content=AA.BBB.C
. Первым делом иду на
jwt.io
, чтобы посмотреть, что внутри.
https://forum.antichat.xyz/attachments/4920546/img_64e8a3a2b5.png
И тут первый "красный флаг". Внутри токена, в открытом виде (просто закодированном в Base64), лежат поля
login
и
password
. Да, пароль в виде хэша (похоже на Blowfish с солью), но сама идея хранить хэш пароля в JWT — это дикая анти-практика.
Важно понимать: JWT по умолчанию не шифруется, а подписывается. Это как паспорт: любой может прочитать ваши данные, но подделать печать (цифровую подпись) без секретного ключа, который есть только у сервера, невозможно. Так что идея подделать токен "в лоб", не зная секрета, отпадает сразу. Но сам факт такой архитектуры уже говорит о многом.
Прощупываем очевидные векторы
Стандартные креды: Приложение ставится с дефолтным логином
admin
. Пароль генерируется, 12 символов, брутить бесполезно.
Перехват трафика: Соединение по умолчанию идет по HTTP. Теоретически можно было бы провернуть MitM-атаку, взломав Wi-Fi, но сотрудники сидят через USB-модемы. Вектор отпадает.
Защита от брутфорса: Пробую перебирать пароли для
admin
. Пять неудачных попыток — и мой IP в бане. Попытка обойти бан через заголовок
X-Forwarded-For
ни к чему не приводит. Сервер на него не реагирует. Без пула прокси тут делать нечего.
Публичные уязвимости: Гуглю название софта + "vulnerability". Нахожу отчёт восьмилетней давности о классической SQL-инъекции в cookie для обхода аутентификации:
content=text' OR '1'='1
. Пробую — дыра давно закрыта. Запускаю на этот параметр Intruder в Burp'е с пачкой популярных пейлоудов для SQLi — тоже безрезультатно.
Раз с аутентификацией всё так глухо, пора посмотреть, что происходит внутри приложения. Векторов два: атаки на серверную часть (RCE, LFI) и на клиентскую (XSS, CSRF).
XSS и CSRF
Проверяю на XSS. В дашборде отображаются разные данные, приходящие в
GET
-параметрах. Загоняю туда всевозможные пейлоуды из популярных списков — приложение всё добросовестно экранирует. Украсть cookie админа через XSS не выйдет.
А что насчёт CSRF (Cross-Site Request Forgery)? По мне так крайне скучная уязвимость, до боли банальная. Но именно на таких банальностях часто и прокалываются. Идея проста: заставить залогиненного пользователя перейти по нашей ссылке, и его браузер сам, от его имени, выполнит нужное нам действие.
Смотрю на критически важное действие — создание нового администратора. И что я вижу? А точнее, чего я не вижу? Никакой защиты от CSRF. Ни случайного токена, ни проверки заголовков. Ничего.
Запрос на создание админа выглядит так:
POST /admin/?object=account.create
с телом
application/json
:
https://forum.antichat.xyz/attachments/4920546/img_118f9d7840.png
Тут и кроется главная загвоздка. Обычная HTML-форма не может отправить запрос с
Content-Type: application/json
. Но есть старый трюк. С ним пришлось повозиться, но в итоге родился рабочий PoC.
HTML:
// Просто для удобства, можно и на чистом JS
// но jQuery гарантирует срабатывание на большинстве браузеров
$(document).ready(function() {
// Вся магия здесь. Мы меняем атрибут name на строку, которая является телом JSON.
// enctype="text/plain" заставит браузер отправить это как: {"type":...="me"}
// Нестрогий парсер на сервере съест это и обработает как валидный JSON.
$("#json_payload").attr("name", '{"type":"ADMIN","preferences":{"timezone":"EST","language":"ca"},"login":"hacked_admin","new_password":"P@ssw0rd123","new_password_confirmation":"P@ssw0rd123"}{"comment":"');
// Отправляем форму без ведома пользователя
$("#csrf_form").submit();
});
Вот такой костыль. Выглядит странно, но именно эта вариация стабильно работала после всех тестов. Серверный парсер оказался достаточно "гибким", чтобы проглотить такой запрос.
Доставка
Эксплойт готов и протестирован в лабе. Осталось доставить его жертве. Подозрительные ссылки никто открывать не будет. Поэтому покупаем домен, созвучный с рабочей тематикой конторы, накидываем простенький сайт-визитку, внедряем наш скрипт.
Дальше — дело техники. Составляем деловое предложение, отправляем на корпоративную почту и делимся ссылкой на наш "рабочий" сайт. Администратор кликает, переходит на сайт... и в этот момент на заднем фоне без его ведома срабатывает наш скрипт. Его браузер, сохранивший легитимную сессию, отправляет запрос на создание нового админа с логином
hacked_admin
. Вуаля, атака прошла успешно.
Как защититься?
В первую очередь, разработчики могли бы этого избежать, если бы просто знали о существовании таких атак. Продукт на рынке с десятых годов... могли бы и заказать аудит приложения
Но если серьезно, вот конкретные шаги для защиты от подобной CSRF-атаки на JSON API:
SameSite Cookies: Самый эффективный современный метод. Установить для сессионной cookie атрибут
SameSite=Strict
или
SameSite=Lax
. В режиме
Lax
(дефолт во многих браузерах) cookie не будут отправляться при межсайтовых POST-запросах. Это убило бы нашу атаку на корню.
Anti-CSRF токены: Классика. Генерировать случайный токен для каждой сессии, встраивать его в защищаемые формы/запросы и проверять на сервере.
Проверка заголовка
Content-Type
: Сервер должен строго проверять, что
Content-Type
входящего запроса — именно
application/json
. Наш трюк с
text/plain
бы не прошел.
Проверка заголовков
Origin
или
Referer
: Дополнительный рубеж обороны. Сервер может проверять, что запрос пришел с доверенного домена, а не с
evil.com
.
Я остался доволен своей работой. А бывшее начальство получило хороший (и дорогой) урок по кибербезопасности.
https://forum.antichat.xyz/attachments/4920546/img_23b4c6740f.png
ДИСКЛЕЙМЕР
Эта история — художественный вымысел. Все совпадения с реальными людьми или уязвимостями — случайность. Материал предоставлен исключительно в образовательных целях для специалистов по ИБ. Повторение подобных действий в реальной жизни приведет к серьезным проблемам с законом. Помните: вы не Вавилен.
Лирическое отступление
Вавилен был доволен своим рабочем местом в конторе. Он приносил деньги себе и своему начальству, которое давало для этого всю инфраструктуру. Но в один прекрасный день боссы, в очередной раз срубив куш на труде Вавилена, решили с ним не делиться. Так они потеряли ценного сотрудника и получили в ответ обманутого и очень мотивированного человека, жаждущего возмездия.
Постановка цели
После ухода из конторы все доступы к инфраструктуре, разумеется, были отозваны. Но это не проблема. Основные активы бизнеса крутятся в кастомном веб-приложении для обработки данных. Компрометация этого приложения — ключ ко всей нужной информации. В истории браузера, к счастью, остался IP-адрес сервера. С него и начнём.
Изучение цели
Чтобы не шуметь и не светить свой IP на боевом сервере, было принято очевидное решение — развернуть аналогичный софт у себя в лаборатории. ПО распространяется по подписке, но есть семидневный триал с предоставлением VPS. Идеально для наших тестов. Регистрируюсь через 10-минутную почту, разворачиваю софт, активирую триал. Можно приступать к вскрытию.
Первое, что нас встречает — стандартная форма логина.
https://forum.antichat.xyz/attachments/4920546/img_00cf84fdc3.png
Смотрю, как устроен механизм аутентификации. При попытке входа улетает
POST
запрос с телом в формате
application/json
. Ничего необычного.
Что там в JWT?
После успешного входа сервер в ответ ставит cookie с JWT:
content=AA.BBB.C
. Первым делом иду на
jwt.io
, чтобы посмотреть, что внутри.
https://forum.antichat.xyz/attachments/4920546/img_64e8a3a2b5.png
И тут первый "красный флаг". Внутри токена, в открытом виде (просто закодированном в Base64), лежат поля
login
и
password
. Да, пароль в виде хэша (похоже на Blowfish с солью), но сама идея хранить хэш пароля в JWT — это дикая анти-практика.
Важно понимать: JWT по умолчанию не шифруется, а подписывается. Это как паспорт: любой может прочитать ваши данные, но подделать печать (цифровую подпись) без секретного ключа, который есть только у сервера, невозможно. Так что идея подделать токен "в лоб", не зная секрета, отпадает сразу. Но сам факт такой архитектуры уже говорит о многом.
Прощупываем очевидные векторы
Стандартные креды: Приложение ставится с дефолтным логином
admin
. Пароль генерируется, 12 символов, брутить бесполезно.
Перехват трафика: Соединение по умолчанию идет по HTTP. Теоретически можно было бы провернуть MitM-атаку, взломав Wi-Fi, но сотрудники сидят через USB-модемы. Вектор отпадает.
Защита от брутфорса: Пробую перебирать пароли для
admin
. Пять неудачных попыток — и мой IP в бане. Попытка обойти бан через заголовок
X-Forwarded-For
ни к чему не приводит. Сервер на него не реагирует. Без пула прокси тут делать нечего.
Публичные уязвимости: Гуглю название софта + "vulnerability". Нахожу отчёт восьмилетней давности о классической SQL-инъекции в cookie для обхода аутентификации:
content=text' OR '1'='1
. Пробую — дыра давно закрыта. Запускаю на этот параметр Intruder в Burp'е с пачкой популярных пейлоудов для SQLi — тоже безрезультатно.
Раз с аутентификацией всё так глухо, пора посмотреть, что происходит внутри приложения. Векторов два: атаки на серверную часть (RCE, LFI) и на клиентскую (XSS, CSRF).
XSS и CSRF
Проверяю на XSS. В дашборде отображаются разные данные, приходящие в
GET
-параметрах. Загоняю туда всевозможные пейлоуды из популярных списков — приложение всё добросовестно экранирует. Украсть cookie админа через XSS не выйдет.
А что насчёт CSRF (Cross-Site Request Forgery)? По мне так крайне скучная уязвимость, до боли банальная. Но именно на таких банальностях часто и прокалываются. Идея проста: заставить залогиненного пользователя перейти по нашей ссылке, и его браузер сам, от его имени, выполнит нужное нам действие.
Смотрю на критически важное действие — создание нового администратора. И что я вижу? А точнее, чего я не вижу? Никакой защиты от CSRF. Ни случайного токена, ни проверки заголовков. Ничего.
Запрос на создание админа выглядит так:
POST /admin/?object=account.create
с телом
application/json
:
https://forum.antichat.xyz/attachments/4920546/img_118f9d7840.png
Тут и кроется главная загвоздка. Обычная HTML-форма не может отправить запрос с
Content-Type: application/json
. Но есть старый трюк. С ним пришлось повозиться, но в итоге родился рабочий PoC.
HTML:
// Просто для удобства, можно и на чистом JS
// но jQuery гарантирует срабатывание на большинстве браузеров
$(document).ready(function() {
// Вся магия здесь. Мы меняем атрибут name на строку, которая является телом JSON.
// enctype="text/plain" заставит браузер отправить это как: {"type":...="me"}
// Нестрогий парсер на сервере съест это и обработает как валидный JSON.
$("#json_payload").attr("name", '{"type":"ADMIN","preferences":{"timezone":"EST","language":"ca"},"login":"hacked_admin","new_password":"P@ssw0rd123","new_password_confirmation":"P@ssw0rd123"}{"comment":"');
// Отправляем форму без ведома пользователя
$("#csrf_form").submit();
});
Вот такой костыль. Выглядит странно, но именно эта вариация стабильно работала после всех тестов. Серверный парсер оказался достаточно "гибким", чтобы проглотить такой запрос.
Доставка
Эксплойт готов и протестирован в лабе. Осталось доставить его жертве. Подозрительные ссылки никто открывать не будет. Поэтому покупаем домен, созвучный с рабочей тематикой конторы, накидываем простенький сайт-визитку, внедряем наш скрипт.
Дальше — дело техники. Составляем деловое предложение, отправляем на корпоративную почту и делимся ссылкой на наш "рабочий" сайт. Администратор кликает, переходит на сайт... и в этот момент на заднем фоне без его ведома срабатывает наш скрипт. Его браузер, сохранивший легитимную сессию, отправляет запрос на создание нового админа с логином
hacked_admin
. Вуаля, атака прошла успешно.
Как защититься?
В первую очередь, разработчики могли бы этого избежать, если бы просто знали о существовании таких атак. Продукт на рынке с десятых годов... могли бы и заказать аудит приложения
Но если серьезно, вот конкретные шаги для защиты от подобной CSRF-атаки на JSON API:
SameSite Cookies: Самый эффективный современный метод. Установить для сессионной cookie атрибут
SameSite=Strict
или
SameSite=Lax
. В режиме
Lax
(дефолт во многих браузерах) cookie не будут отправляться при межсайтовых POST-запросах. Это убило бы нашу атаку на корню.
Anti-CSRF токены: Классика. Генерировать случайный токен для каждой сессии, встраивать его в защищаемые формы/запросы и проверять на сервере.
Проверка заголовка
Content-Type
: Сервер должен строго проверять, что
Content-Type
входящего запроса — именно
application/json
. Наш трюк с
text/plain
бы не прошел.
Проверка заголовков
Origin
или
Referer
: Дополнительный рубеж обороны. Сервер может проверять, что запрос пришел с доверенного домена, а не с
evil.com
.
Я остался доволен своей работой. А бывшее начальство получило хороший (и дорогой) урок по кибербезопасности.