Решаем CTF #1 - JWT tokens
By PyProHACK - tg: @pyprohack_projectПривет! Сегодня изучим что такое JWT токен и изучим уязвимости, которые допускают разработчики веб приложений(и не только на самом деле) при его использовании. Начнём с теории, а затем решим несколько задач с root-me.org

Что это за зверь такой, JWT?
JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения подлинности аккаунта.
JWT - это специальный токен, который используется для аутентификации клиентов в приложении. Пайплайн работы с данным токеном на примере простого веб-приложения:
- Клиент авторизуется по паре логин-пароль и получает в ответ jwt токен
- Этот токен сохраняется в браузере пользователя (например в localStorage)
- Теперь в каждом запросе к серверу в хэдэре (обычно это Authorization) клиент передаёт полученный ранее токен
- Токен однозначно идентифицирует клиента(об этом позже), поэтому сервер знает, кто отправляет запрос (например id пользователя в бд). Теперь сервер может например возвращать содержимое страницы, если у пользователя с таким id есть необходимые права
Данный подход для аутентификации клиентов позволяет в разы упростить разработку, сделать её независимой от базы данных и вообще самого приложения(очень часто используется в микро-сервисной архитектуре).
Структура JWT
JWT токен состоит из трёх частей, разделённых точкой. Каждая часть - json строка, закодированная в base64. Пример токена:

Первая часть токена: header; в ней хранится тип токена (JWT) и тип шифрования, которым токен подписывается, чтобы токен мог подписать только сервер и можно было легко отличить оригинальный токен от подделки.
Вторая часть токена: payload; в этой части хранится основная информация, идентифицирующая пользователя(id, email или что - то ещё), так же ещё добавляют время жизни токена.
Третья часть: подпись; как было сказано ранее, jwt токен подписывается, что не позволяет злоумышленникам генерировать фейковые токены и получать доступ к аккаунтам других пользователей.
Как вы уже могли догадаться, основные уязвимости кроются в том, что злоумышленники генерируют фейковые токены, в которых находится информация для авторизации других пользователей.
Поэтому очень важно использовать криптостойкий ключ шифрования, которым подписывается токен, а так же использовать надёжный метод шифрования.
Переходим от теории к практике!
JWT - Introduction

В данной задаче нас просят залогиниться от имени админа. Задача на jwt токен, соответственно скорее всего от нас требуется сгенерировать фейковый токен, который пропустит сервер.
Переходим на сайт и видим форму входа. Внизу формы есть кнопка гостевого входа. Скорее всего она сгенерирует какой - то jwt токен и у нас будет с чем работать. Перед тем как входить не забываем открывать "Network monitor" в браузере или "Burp Suite".

Видим, что в запросе, который выполнился после входа наш браузер в куки передал jwt токен.

Теперь проанализируем его. Я использую утилиту на Python - JWT Tool, но если вы не хотите ничего устанавливать, то можете открыть сайт jwt.io
Установка JWT Tool (пригодится в следующих задачах)
Утилита работает на Linux, MacOS, Windows. Для этого у вас должен быть установлен Python 3 и pip.
Пример для Linux (с установленным git):
git clone https://github.com/ticarpi/jwt_tool && cd jwt_tool pip3 install -r requirements.txt
Запуск утилиты
python3 jwt_tool.py
Смотрим содержимое токена через jwt tool

Или через jwt.io

Видим, что в payload, используется username - guest. Логично предположить, что заменив его на admin мы получим доступ к админу.
Но просто так заменить guest на admin и закинуть новый токен в куки не получится. Всё дело в том, что текущий токен подписан с использованием HS256(по сути sha256). Если бы мы знали ключ по которому подписывали этот токен, то всё бы получилось.
Плохо настроенные системы аутентификации по jwt токенам могут принимать не подписанные токены. У таких токенов в хэдере в типе шифрования("alg") находится просто "none".
Также у них отсутствует третья часть.
Отредактировать содержимое можно вручную, заменив вторую часть на json закодированный в base64, и аналогичным образом отредактировать хэдер. Но я предпочитаю использовать jwt tool.
Запускаем её с флагом -T.
python3 jwt_tool.py ТОКЕН -T
Вместо ТОКЕН вставляйте свой jwt токен, полученный на прошлых шагах.
Далее выбираем пункты, как на скриншоте ниже:

В итоге вы получите отредактированный jwt токен. Вы можете заметить, что он всё так же состоит из 3 частей. Дело в том, что jwt_tool не знает как подписать токен, так как мы не передали её необходимую информацию (тип и ключ), поэтому в качестве третьей части просто взяла подпись оригинального токена, что конечно же не корректно. В нашем случае копируем только первые 2 части токена.

Теперь нам осталось просто подменить отправляемый токен. Можно вручную через curl отправить запрос с нужным токеном, но разбирать html, который мы получим на выходе неудобно, поэтому воспользуемся штатным функционалом браузера(либо burp suite).
В Developer Tools открываем вкладку Application. В ней выбираем Storage->Cookies->http://challenge01.root-me.org/

В таблице видим поле с названием jwt, 2 раза кликаем по значению и вставляем туда наш новый токен.

Теперь обновляем страницу и видим флаг:

Согласно правилам сайта root-me.org запрещено размещать флаги на задания, поэтому я его замазал.
Отправляем флаг на валидацию и получаем 20 баллов.
JWT - Weak secret

Итак, у нас есть доступ к api, который на эндпоинте /hello отправляет следующее сообщение:
Let's play a small game, I bet you cannot access to my super secret admin section. Make a GET request to /token and use the token you'll get to try to access /admin with a POST request.
Собственно мы можем получить токен на эндпоинте /token и далее с этим токеном нам необходимо отослать POST запрос на /admin.
Пробуем (делаю всё через curl). Важно аккуратно написать хэдер со пробелами в нужных местах:
Authorization: Bearer ТОКЕН

Собственно получили токен и на эндпоинте /admin видим намёк на тип токена, но никаким флагом здесь и не пахнет. Давайте разбирать содержимое токена. Прогоняем его через jwt tool или jwt.io:

Судя по всему нам надо изменить роль с guest на admin. Для подписи используется HS512(как нам и сказали ранее).
Можно попробовать убрать подпись токена, как мы делали в предыдущем задании, но смысла в этом нет. Ничего не получится. Здесь такой уязвимости очевидно нет.
Задание называется "Weak secret", что аккуратно намекает на использование слабого ключа шифрования, при подписывании токенов, которые выдаёт нам челлендж. Раз ключ слабый, то значит его можно попробовать забрутфорсить. Благо такая функция есть в jwt_tool.
Для этого запустим jwt_tool со следующими параметрами:
python3 jwt_tool.py ТОКЕН -C -d ../rockyou.txt
Флаг -C показывает, что нужно крякнуть токен
Флаг -d показывает путь к вордлисту по которому необходимо совершить брутфорс. Я взял банальный rockyou.

Пара секунд и ключ подобран: lol
Теперь мы можем изменить токен и подписать его так, чтобы сайт принял его, как свой.
Воспользуемся jwt_tool со следующими аргументами:
python3 jwt_tool.py ТОКЕН -T -S hs512 -p "lol"

В итоге получим валидный токен. Пробуем отправить запрос с ним и получаем флаг.

В конце флага будет \n, он не относится к флагу. Сдаём и получаем 25 баллов.
JWT - Public key

Нам дают api с тремя эндпоинтами. Посмотрим, что возвращает /key

Мы получили публичный ключ, скорее всего через него подписываются все jwt токены в это челлендже.
На /auth просят отправить юзернейм

Отправляем admin

Нам не верят. Пробуем что - то другое и получаем jwt токен.

Тут по классике анализируем содержимое токена:

Подписан через RS256, нам нужно заменить username на admin.
Что такое RS256? По сути это RSA SHA256. RSA - асимметричный алгоритм шифрования, поэтому нам не зря дали публичный ключ. Мы можем попробовать заменить тип подписи с RS256 на SHA256. А в качестве ключа для подписи взять публичный ключ, полученный ранее.
Если сервер декодит токен по публичному ключу, то при получении синхронного токена, он рассмотрит его, как синхронный, а значит пропустит фейковый токен.
Через jwt_tool заменим тип подписи на HS256(либо любой другой симметричный алгоритм) и поменяем юзернейм на admin
python3 jwt_tool.py ТОКЕН -T

Из полученного ключа берём только хэдер и пейлоад(2 первые части).
Теперь переведём публичный ключ, полученный к эндпоинта /key в hex, это нужно для утилиты openssl, она не сможет подписать с другим. Самое просто перекинуть токен в любой hex editor. Например, Bless, либо xxd.

Вы получите такой ключ:
2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b43415145417576773168347477504a6e5a42772b54327743440a59624832556b4d427852672f686b534d6c365a77693259566d37397771723372506433676a7430695a576432724e42337175644b5749536d42516132517152480a74503666546a6569354d413471734c53586c32724765576a47767471704851446d63583447417841454b7947306e6632445065324170454330323152472f564f0a64595343414149702b536d6443746d35504966314153694f4141585537644c37324959736f63534d6759705249634a755a5341435571314a367775553958796c0a7042314e7657774644334659437a72655435416b5469636276676546316b39792f4f4b5431667632626e5347706a354b4d45462f51575552377877337262516b0a796e6365436a714645744c78584873584a506d5536676a4132494377547131475671435a447a6a6a665162426f4b7a6d59534f774c7a4f6e364a6237454f50670a58514944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a
Теперь подпишем наш токен и получим его третью часть следующей командой:
echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0" | openssl dgst -sha256 -mac HMAC -macopt hexkey: 2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b43415145417576773168347477504a6e5a42772b54327743440a59624832556b4d427852672f686b534d6c365a77693259566d37397771723372506433676a7430695a576432724e42337175644b5749536d42516132517152480a74503666546a6569354d413471734c53586c32724765576a47767471704851446d63583447417841454b7947306e6632445065324170454330323152472f564f0a64595343414149702b536d6443746d35504966314153694f4141585537644c37324959736f63534d6759705249634a755a5341435571314a367775553958796c0a7042314e7657774644334659437a72655435416b5469636276676546316b39792f4f4b5431667632626e5347706a354b4d45462f51575552377877337262516b0a796e6365436a714645744c78584873584a506d5536676a4132494377547131475671435a447a6a6a665162426f4b7a6d59534f774c7a4f6e364a6237454f50670a58514944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a
Получим токен:
f4a3602e56b0b5d07229d66130ef0aedd1d338cbdc543641d0bb8bedaf8f65ba
Переведём его в base64(предварительно вернув из hex в нормальный вид). Для этого воспользуемся следующим скриптом на Python:
import base64, binascii
print(base64.urlsafe_b64encode(binascii.a2b_hex("f4a3602e56b0b5d07229d66130ef0aedd1d338cbdc543641d0bb8bedaf8f65ba")).replace('=',''))
В итоге соединим всё вместе и получим итоговый jwt токен:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.9KNgLlawtdByKdZhMO8K7dHTOMvcVDZB0LuL7a-PZbo
Отправляем запрос с этим токеном и получаем заветный флаг.
На этом всё!