Сегодня я поделюсь недавней интересной уязвимостью. Однако я не могу раскрыть название программы и домен, так как не получил разрешения на их публикацию.

Предположим, что целью является 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.

Этапы:

  1. Перейдите на сайт: https://test.com/.

  2. Войдите в свою учётную запись. Измените параметр 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 :)

Шаги:

  1. Перейдите на test.com

  2. Войдите в свою учетную запись.

  3. Проверьте заголовок 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 тысячи долларов.

Я надеюсь, что сегодня вы узнали что-то новое, и прошу прощения, если мой отчет оказался недостаточно ясным или если я слишком много говорил.

Life-Hack - Жизнь-Взлом

Комментарии (8)


  1. GerrAlt
    19.11.2024 20:01

    Я что-то не понял, JWT токен имеет подпись, если в нем подменить значение поля - подпись станет недействительной

    Это история про то что кто-то использовал JWT без проверки подписи?


    1. Schavelev
      19.11.2024 20:01

      Насколько я понимаю каждый раз JWT подписывался корректно, просто значение realm бралось любое, которое передано в POST запросе и можно было его менять.


    1. 0x22
      19.11.2024 20:01

      Автор делал запрос, в котором мог менять параметр, а сервер уже генерил токен и отдавал валидный JWT с этим параметром.


    1. jonic
      19.11.2024 20:01

      Я больше не понял как убирание bearer позволило принять токен за админский -_-


      1. Sap_ru
        19.11.2024 20:01

        Скорее всего где-то косят в парсинге/регулярный выражениях был и без bearer сравнение успешно проходило. Например так: что-то где-то при разборе возвращало "bearer" и токен. И если вместо "bearer" получался какой-нибудь null и дальше было кривое сравнение на JS/TS (какое-нибудь сравнение с отрицанием), то результат вполне могу получаться "true".
        А может быть где-то внутри исключение выбрасывалось и тихо перехватывалось (отлаживали и забыли убрать). Но это жёсткий прохлоп.


  1. su1ts
    19.11.2024 20:01

    Очень интересно, спасибо за статью!


  1. zBornss
    19.11.2024 20:01

    Очень очень приятный пост. Пиши ещё)


  1. arsen_gl
    19.11.2024 20:01

    Автору спасибо, за перевод! Читается будто книга в стиле киберпанк детектива. Но я не могу найти, где автор нашел список realm'ов. В исходниках?