Сегодня я поделюсь недавней интересной уязвимостью. Однако я не могу раскрыть название программы и домен, так как не получил разрешения на их публикацию.
Предположим, что целью является test.com.
Начав тестирование программы, я нашел способ обхода пользовательского интерфейса административной панели. Цель использует JSON Web Token (JWT) в качестве механизма аутентификации. Я уделил немало времени, чтобы разобраться и выявить возможные уязвимости на объектах программы, использующих JWT.
При входе на основной сайт test.com, для обычного пользователя генерируется JWT.
После изучения работы цели я начал собирать данные:
Читал JavaScript-файлы.
Использовал Burp Suite для анализа запросов.
Переходил по кнопкам на сайте.
Использовал Wayback Machine для поиска всех возможных конечных точек.
Проводил перечисление поддоменов.
В результате я обнаружил интересный поддомен admin.test.com.
На поддомене admin.test.com был открыт JavaScript-файл app.js. Прочитав его (200 000 строк кода), я выяснил, что он также использует JWT для аутентификации.
Кроме того, я нашел список realm. В нём нашлось кое-что интересное — test-dashboard.
Что такое realm?
Параметр "realm" в аутентификации используется для указания области защиты. Пространство защиты определяется каноническим корневым URI (схемой и компонентами authority запрашиваемого URI).
Подробнее можно прочитать в RFC 7235, раздел 2.2:
https://www.rfc-editor.org/rfc/rfc7235#section-2.2
Я использовал jwt.io для декодирования токена пользователя. В токене был указан realm=test-user.
Теперь я предположил, что если смогу изменить realm на test-dashboard, то смогу войти в административную панель.
test-dashboard — это имя веб-сайта, которое заменяет test, то есть оно выглядело как: target-dashboard.
Этапы:
Перейдите на сайт: https://test.com/.
Войдите в свою учётную запись. Измените параметр realm на test-dashboard в POST-запросе: https://test.com/api/v1/login
HTTP request
POST /api/v1/login HTTP/1.1
Host: accounts.test.com
Connection: close
Content-Length: 79
Accept: */*
Content-Type: application/json
{“email”:”youremail@gmail.com”,”password”:”<password>”,”realm”:”test-dashboard”}
Если декодировать JWT, можно увидеть, что параметр realm был изменён.
Теперь, используя модифицированный JWT-токен, я смог получить доступ к административной панели.
Я немедленно сообщил об этой уязвимости, но получил вполне ожидаемый ответ от программы Bug Bounty:
Мы обсудили это с разработчиками, и они заявили, что административная панель, к которой вы получили доступ, представляет собой всего лишь React-приложение, отрендеренное на стороне клиента (страницы, которые используют только публичную информацию для отображения), и ничего более. Фактический API является отдельным приложением с эндпоинтами, которые требуют действительного токена авторизации с определёнными правами доступа. Таким образом, если вы не сможете создать токен, который позволит вам взаимодействовать с API, уровень серьёзности данной проблемы будет низким.
Они изменили уровень серьёзности проблемы с Critical на Medium.
Я был готов сдаться, но решил продолжить копать глубже.
Согласившись с командой, я понял, что для того, чтобы считать уязвимость критической, мне нужно манипулировать параметром scope в токене JSON Web Token (JWT).
Однако это казалось невозможным, поскольку для этого потребовалась бы эксплуатация 0-day уязвимости в механизме JWT, что сделало бы уязвимыми любые сайты, использующие JSON Web Token (JWT).
Но я был достаточно настойчив, чтобы продолжить искать что-то подобное.
Так как я мог управлять параметром realm и генерировать валидные JWT-токены, я попробовал всевозможные полезные нагрузки для манипуляции scope, но ничего не работало, и мне не удалось добиться нужного результата.
Затем я начал поиск контента с использованием ffuf на поддомене admin.test.com, но, к сожалению, не нашел никаких валидных точек.
По умолчанию ffuf использует HTTP-метод GET, поэтому я решил попробовать метод POST. И я обнаружил https://admin.test.com/upload, который возвращал 403 Forbidden. Это показалось мне интересным, так как эта ссылка упоминался в файле app.js.
Тогда я подумал: "А что, если я смогу загрузить веб-оболочку?" Эта идея меня очень воодушевила.
После нескольких часов изучения JavaScript-файла мне удалось составить запрос для загрузки файла:
POST /upload HTTP/1.1
Host: admin.test.com
Connection: close
Content-Length: 300
Accept: application/json, text/plain, */*
Content-Type: multipart/form-data; boundary=----
WebKitFormBoundarypxxxxxx
Authorization: Bearer <JWT>
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4629.0 Safari/537.36
------WebKitFormBoundarypxxxxxx
Content-Disposition: form-data; name="destination"
gallery/
------WebKitFormBoundarypxxxxxx
Content-Disposition: form-data; name="file"; filename="poc.txt"
Content-Type: Text/plain
h4x0r-dz POC
------WebKitFormBoundarypxxxxxx--
Но я получил ошибку 401 HTTP :(, даже после того как я манипулировал realm в JWT.
Обход аутентификации
Знаете ли вы, что такое Фаззинг?
Если ваш ответ — НЕТ, то вы упустили множество ошибок, которые могут быть найдены!
Фаззинг (или Fuzz-тестирование) — это метод “черного ящика” тестирования программного обеспечения, который, по сути, заключается в нахождении ошибок реализации с помощью автоматизированной инъекции.
Я начал фаззинг заголовка Authorization: Bearer <JWT>, и наконец я увидел ответ 200 :)
Шаги:
Перейдите на test.com
Войдите в свою учетную запись.
Проверьте заголовок Authorization
Проблема заключается в том, что если вы удалите слово Bearer из заголовка Authorization, вы сможете пройти аутентификацию на https://admin.test.com и получить права администратора.
Отправьте этот запрос для загрузки файла.
POST /upload HTTP/1.1
Host: admin.test.com
Connection: closeContent-Length: 300
Accept: application/json, text/plain, */*
Content-Type: multipart/form-data; boundary=----
WebKitFormBoundarypxxxxxx
Authorization: <JWT>
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4629.0 Safari/537.36
------WebKitFormBoundarypxxxxxx
Content-Disposition: form-data; name="destination"
gallery/
------WebKitFormBoundarypxxxxxxContent-Disposition: form-data; name="file"; filename="poc.txt"
Content-Type: Text/plain
h4x0r-dz POC
------WebKitFormBoundarypxxxxxx--
Я получил ответ 200 OK с сообщением uploaded.
Теперь возникает вопрос: где найти путь к моему файлу?
Сначала я подумал, что это конец, поскольку невозможно определить, куда был загружен мой файл.
Я попробовал контентную разведку с помощью ffuf для всех поддоменов, пытаясь найти что-то вроде admin.test.com/uploads/poc.txt, но ничего не обнаружил.
Затем я начал просматривать историю запросов в Burp Suite и анализировать ответы. В одном из них я обнаружил следующую строку:
href=https://xxxxxxxx.cloudfront.net/gallery/xxxxxxxxx
Интересно, что gallery совпадает со значением, которое я передавал в поле destination при загрузке файла.
Я перешел по адресу:
https://xxxxxxxxx.cloudfront.net/gallery/poc.txt
И обнаружил, что мой файл находится именно там.
Но что такое CloudFront?
Amazon CloudFront — это сеть доставки контента (CDN), предоставляемая Amazon Web Services. Сети доставки контента обеспечивают глобально распределенную сеть прокси-серверов, которые кэшируют контент, такой как веб-видео или другие ресурсоемкие данные, улучшая скорость доступа к скачиваемому контенту.
Таким образом, загрузить веб-оболочку не получится :(
Даже если я сейчас сообщу о данной уязвимости загрузки файла, её уровень серьезности будет крайне низким. Поэтому я продолжил копать глубже.
Перезапись произвольного файла
По умолчанию Amazon S3 уязвим к некорректной конфигурации, позволяющей осуществить перезапись произвольного файла. Например, если загрузить file.txt, это приведет к его перезаписи на Amazon S3.
Теперь у меня есть возможность перезаписи произвольного файла. Это открывает множество возможностей.
Я обнаружил, что xxxxxxxx.cloudfront.net используется на основном сайте для размещения JavaScript, HTML и других файлов.
Множество файлов размещается на xxxxxxxx.cloudfront.net, и как атакующий, я могу изменять содержимое этих файлов. Это позволило мне получить stored XSS и другие уязвимости безопасности на основном домене, потому что они использовали xxxxxxxx.cloudfront.net для размещения программного обеспечения Windows и PDF-файлов. Эти файлы являются частью основного сайта.
Таким образом, я могу изменять содержимое этих файлов и получить удалённое выполнение кода (RCE) на компьютерах пользователей, внедряя вредоносный код в существующие EXE или PDF файлы, CSS и т.д.
Первый POC файл:
Мы не могли подтвердить уязвимость через браузер из-за кэширования, поэтому можно использовать CURL.
Теперь, я изменил содержимое файла poc.txt через этот запрос:
POST /upload HTTP/1.1
Host: admin.test.com
Connection: closeContent-Length: 300
Accept: application/json, text/plain, */*
Content-Type: multipart/form-data; boundary=----
WebKitFormBoundarypxxxxxx
Authorization: <JWT>
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4629.0 Safari/537.36
------WebKitFormBoundarypxxxxxx
Content-Disposition: form-data; name="destination"
gallery/
------WebKitFormBoundarypxxxxxx
Content-Disposition: form-data; name="file"; filename="poc.txt"
Content-Type: Text/plainArbitrary
File Overwrite
------WebKitFormBoundarypxxxxxx--
Как видно из моего терминала, мне удалось перезаписать существующий файл.
Ответ от команды:
Я получил 20 тысяч долларов за эту уязвимость.
Также я получил 3000 долларов за доступ к панели администратора, в сумме получилось 23 тысячи долларов.
Я надеюсь, что сегодня вы узнали что-то новое, и прошу прощения, если мой отчет оказался недостаточно ясным или если я слишком много говорил.
Комментарии (8)
arsen_gl
19.11.2024 20:01Автору спасибо, за перевод! Читается будто книга в стиле киберпанк детектива. Но я не могу найти, где автор нашел список realm'ов. В исходниках?
GerrAlt
Я что-то не понял, JWT токен имеет подпись, если в нем подменить значение поля - подпись станет недействительной
Это история про то что кто-то использовал JWT без проверки подписи?
Schavelev
Насколько я понимаю каждый раз JWT подписывался корректно, просто значение realm бралось любое, которое передано в POST запросе и можно было его менять.
0x22
Автор делал запрос, в котором мог менять параметр, а сервер уже генерил токен и отдавал валидный JWT с этим параметром.
jonic
Я больше не понял как убирание bearer позволило принять токен за админский -_-
Sap_ru
Скорее всего где-то косят в парсинге/регулярный выражениях был и без bearer сравнение успешно проходило. Например так: что-то где-то при разборе возвращало "bearer" и токен. И если вместо "bearer" получался какой-нибудь null и дальше было кривое сравнение на JS/TS (какое-нибудь сравнение с отрицанием), то результат вполне могу получаться "true".
А может быть где-то внутри исключение выбрасывалось и тихо перехватывалось (отлаживали и забыли убрать). Но это жёсткий прохлоп.