Веб - самая популярная и самая непредсказуемая категория в CTF. Тут нет формулы «запустил скрипт - получил флаг». Каждый таск - мини-приложение со своей логикой, и побеждает тот, кто быстрее находит отклонение от нормы. За последние пару лет я прошёл несколько сотен веб-тасков на HackTheBox, PicoCTF и Intigriti Monthly Challenges, и вижу три устойчивых столпа: SQL injection, SSRF и insecure deserialization. Обёртки меняются, ядро эксплуатации - нет.
В этом CTF web writeup разберу конкретные подходы с реальных соревнований - с объяснением, почему первые попытки обычно проваливаются и что с этим делать.
Методология разбора веб-тасков в CTF
Прежде чем лезть в конкретные уязвимости, нужна система. Без неё ты потратишь час на фаззинг директорий, когда ответ лежит в комментарии к HTML. Вот мой порядок действий на любом веб-таске:
Первые 60 секунд - открываю сайт в браузере, смотрю исходный код страницы (Ctrl+U), проверяю заголовки ответа через DevTools. Часто прямо в комментариях или в заголовке
уже видно стек: PHP, Flask, Express. Это сразу сужает вектор. На SANReN CTF, например, рекомендуют пробовать обращаться к
,
,
- по ответу сервера понятно, что под капотом.
Следующие 5 минут - если есть исходный код (а в современных CTF его всё чаще дают), читаю серверную часть. Не весь код, а точки входа: роутеры, обработчики POST-запросов, middleware. Именно тут прячутся инъекции. Если исходников нет - запускаю
Код:
ffuf -u http://target/FUZZ -w common.txt
для поиска скрытых эндпоинтов, включая
,
,
.
Когда нашёл потенциальный вектор - проверяю вручную через Burp Suite, а не сразу бросаю автоматику. На MireaCTF, по данным одного из writeup'ов с Antichat, sqlmap не смог найти
blind SQL injection в cookie, хотя уязвимость подтвердилась вручную за минуту. Автоматика хороша, но она не понимает кастомную логику конкретного таска.
В терминах MITRE ATT&CK весь этот процесс - Exploit Public-Facing Application (T1190, Initial Access). Но в CTF мы обычно идём дальше: после первичной эксплуатации нужно прочитать файл, достучаться до внутреннего сервиса или поднять привилегии.
SQL Injection в CTF задачах
SQLi остаётся самой частой уязвимостью в веб-категории CTF. Но если ты думаешь, что это про
- значит давно не играл. Современные таски строятся вокруг обходов фильтров, нестандартных точек инъекции и ситуаций, где автоматика бессильна.
UNION-based: когда видишь вывод
Классический сценарий: приложение выводит данные из базы на страницу, и ты можешь «подклеить» свой запрос через UNION SELECT. Первое, что нужно определить - количество колонок. Я делаю это через
, увеличивая N, пока не получу ошибку. Нашёл, что колонок 3? Отлично:
Код:
' UNION SELECT 1,2,3--
- и смотришь, какая цифра отобразилась на странице. Та позиция - твоё «окно» для извлечения данных.
Типичная ошибка новичков - пытаться сразу читать таблицу
. В CTF структура базы может быть любой. Сначала узнай имена таблиц через
Код:
information_schema.tables
, потом колонки через
Код:
information_schema.columns
. Да, это базовый SQL, но количество людей, которые пропускают этот шаг и тратят время на угадывание, до сих пор поражает.
На HackTheBox в машине Gavel (по данным 0xdf) была интересная штука: SQL injection через PDO с backtick-quoted prepared statements. Стандартные payloads не работали - нужно было разобраться, как именно PDO экранирует входные данные, и найти обход конкретно для backtick-quoting. Вот почему чтение исходников важнее автоматического сканирования.
Blind SQL injection: когда вывода нет
Если приложение не показывает результат запроса, но по-разному реагирует на true/false условия - это blind SQLi. На MireaCTF один из тасков содержал именно такую уязвимость в cookie: при true на странице появлялось слово «Welcome», при false - нет.
Алгоритм ручной эксплуатации: формируешь условие
Код:
' AND SUBSTRING(password,1,1)='a'--
и перебираешь символы. Руками это мучительно медленно, поэтому пишешь скрипт. Но вот подвох: sqlmap с параметрами
Код:
--technique=B --dbms=MySQL --level=5 --risk=3
на том же таске не сработал - точка инъекции была в cookie с нестандартной обработкой. Sqlmap ожидает определённые паттерны ответов, и когда приложение ведёт себя нетипично, приходится писать кастомный эксплойт.
Для sqlmap в таких случаях стоит попробовать сохранить запрос в файл через Burp (
Код:
sqlmap -r request.txt
) и пометить точку инъекции символом
прямо в значении cookie. Полезны флаги
(текст при true) и
Код:
--not-string="Error"
. Но если и это не помогает - пиши свой скрипт на Python с
, бинарным поиском по ASCII-кодам и выводом в реальном времени. Честно, это быстрее, чем воевать с автоматикой.
Извлечение данных через blind SQLi - результат эксплуатации T1190 (Exploit Public-Facing Application, Initial Access). T1213.006 (Databases) описывает сбор данных при легитимном доступе к СУБД, что не про инъекцию.
SSRF в CTF задачах - разбор эксплуатации
Server-Side Request Forgery - мой любимый класс уязвимостей в CTF. Причина проста: SSRF редко бывает конечной целью. Это всегда трамплин - к чтению внутренних файлов, к метаданным облака, к внутренним сервисам без аутентификации. По MITRE ATT&CK начальная эксплуатация SSRF - T1190 (Exploit Public-Facing Application, Initial Access). Сканирование внутренних портов через SSRF (как в примере с Jenkins) - T1046 (Network Service Discovery). T1090 (Proxy) - это про маршрутизацию C2-трафика, к механике SSRF отношения не имеет.
SSRF через Next.js Middleware: реальный кейс CVE-2025-57822
Один из лучших примеров SSRF в CTF - задача CatFlix AI с Intigriti Monthly Challenge (август 2025). Приложение на Next.js, исходный код приложен. Вот как шёл ход мыслей, когда я разбирал аналогичную задачу.
Первое, что бросается в глаза в
- код добавляет security headers к ответу. Но есть блок, который проверяет UTM-параметры в запросе. Когда
присутствует,
вызывается без явной передачи объекта request - и пользовательские заголовки пробрасываются на сервер некорректно. Передача заголовка
вызывает серверный редирект к произвольному URL. Три строчки кода - и вот тебе SSRF.
Это CVE-2025-57822 - SSRF в Next.js до версий 14.2.32 и 15.4.7, CVSS 6.5 (MEDIUM), вектор CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N. Корневая проблема - CWE-918 (Server-Side Request Forgery). Сложность атаки помечена как High (AC:H), потому что нужна специфическая конфигурация middleware - когда
вызывается без передачи request object. В реальных условиях CVSS 6.5 (MEDIUM) отражает именно это ограничение: при пентесте нужно сначала подтвердить наличие уязвимой конфигурации. В CTF авторы задачи это условие гарантируют - спасибо им за это.
Proof of concept элементарный: отправляешь GET-запрос с
и заголовком
Код:
Location: http://localhost:3000/
, и вместо нормального ответа получаешь содержимое внутреннего сервиса. По данным writeup'а с Intigriti, далее через перебор портов (
по диапазону) был найден Jenkins на порту 8080 без аутентификации, а через его Groovy Script Console - выполнение команд:
Код:
Код:
GET /?utm_source=meta HTTP/2
Host: challenge-0825.intigriti.io
Location: http://localhost:8080/script
Content-Type: application/x-www-form-urlencoded
script=println('cat /app/flag.txt'.execute().text)
Цепочка SSRF → internal service discovery → RCE через Jenkins - классический паттерн, который встречается не только в CTF, но и на реальных проектах.
Обход фильтров localhost в CTF
Если авторы таска не полные новички, прямой
будет заблокирован. Вот техники обхода, которые работали у меня на реальных соревнованиях:
DNS rebinding и альтернативные представления IP. Вместо
пробуй
,
,
(decimal),
(IPv6). На MireaCTF один из тасков отдавал флаг при обращении к
- фильтр проверял строку «localhost» и «127.0.0.1», но не альтернативные записи. На заборе тоже написано «вход запрещён».
Редиректы через внешний сервер. Поднимаешь свой сервер, который возвращает 302 на
Код:
http://127.0.0.1:порт
. Приложение проверяет URL до запроса, видит твой домен - всё чисто. Но HTTP-клиент следует за редиректом и попадает на localhost. Для быстрого решения на CTF подходит
или одноразовый Flask-сервер.
URL-схемы. Если SSRF реализован через
или аналог, пробуй
для чтения локальных файлов. Или
для отправки произвольных TCP-пакетов - это позволяет, например, отправить SQL-запрос напрямую в MySQL на localhost:3306 без клиента. Gopher в 2025 году - кто бы мог подумать, что эта штука из 90-х ещё пригодится.
Десериализация в CTF - уязвимость, которую часто пропускают
Insecure deserialization (CWE-502: Deserialization of Untrusted Data, категория A08:2021 - Software and Data Integrity Failures по OWASP Top 10 2021) - это когда приложение восстанавливает объект из пользовательских данных без валидации, и атакующий может подменить данные объекта или вызвать цепочку деструктивных методов. В CTF это встречается в двух основных вариантах: PHP и Java.
PHP: магические методы как точка входа
В PHP десериализация через
опасна из-за магических методов. При вызове
PHP дёргает
(или
в PHP 8.0+, который имеет приоритет). Когда объект уничтожается -
. Если в любом из этих методов есть обращение к файловой системе, выполнение команд или запись данных - это потенциальный RCE.
Моя методика на CTF: нахожу вызов
в исходниках, затем ищу все классы с
,
,
,
. Строю «gadget chain» - цепочку объектов, где свойство одного является объектом другого класса, и при вызове магического метода срабатывает нужная мне логика. Типовой пример: класс
с
, который записывает
с содержимым
. Подменяем
на путь к PHP-файлу,
- на вебшелл, сериализуем, отправляем - и получаем Web Shell (T1505.003, Persistence по MITRE ATT&CK). Красота.
Ещё один важный момент - формат
. Даже если прямого вызова
нет, но есть файловая операция (
,
,
), можно загрузить PHAR-архив с сериализованным объектом в метаданных. При обращении к
Код:
phar://uploads/evil.jpg
PHP автоматически десериализует метаданные - сам, без спроса. Эта техника использовалась, по данным коллекции best-web-ctf-writeups на GitHub, в задаче PDF Creator с CCCamp 2019.
Java: ysoserial и реальность
Java-десериализация - тяжёлая артиллерия. Если приложение принимает сериализованные Java-объекты (маркер
в hex или
в Base64), это почти гарантированный RCE при наличии уязвимых библиотек в classpath.
Инструмент
генерирует payload для известных gadget chains: CommonsCollections, Spring, Groovy и других. Оригинальный ysoserial (frohoff) не поддерживается с ~2021 года и несовместим с Java 17+ из-за ограничений модульной системы - используйте актуальные форки (ysoserial-all, java-deserialization-scanner в Burp). На практике в CTF запускаю
Код:
java -jar ysoserial.jar CommonsCollections5 'команда' | base64
и подставляю результат в уязвимый параметр. Учтите: CommonsCollections5 работает только с commons-collections 3.x; для 4.x нужны chains CommonsCollections2 или CommonsCollections4.
Но не всё так просто - нужно угадать, какая библиотека есть на сервере. Если исходники доступны, смотрю
или
на наличие commons-collections, spring-core, groovy. Если нет - тупо перебираю основные chains. Метод научного тыка, но работает.
На HackTheBox (по данным 0xdf) машины с Java-десериализацией - например, Cereal, Ophiuchi - часто требуют не просто запуск ysoserial, а понимание того, как сериализованные данные обрабатываются конкретным фреймворком. В Ophiuchi уязвимость была в SnakeYAML - парсере YAML для Java, где специально сформированный YAML-документ вызывал загрузку произвольного класса. Десериализация - она такая: вроде один класс уязвимостей, а копнёшь - каждый раз новый зоопарк.
Чейнинг уязвимостей - когда одной баги мало
В хороших CTF задачах один баг не даёт флаг. Нужна цепочка. Вот комбинации, которые я встречал чаще всего:
SSRF + внутренний сервис без аутентификации. Уже разобрали на примере CatFlix AI: SSRF в Next.js middleware → Jenkins без пароля → Groovy RCE. Ищи на внутренних портах Redis (6379), Elasticsearch (9200), Docker API (2375), Kubernetes API (6443).
SQL injection + file read/write. В MySQL через
Код:
LOAD_FILE('/etc/passwd')
или
Код:
INTO OUTFILE '/var/www/html/shell.php'
можно перейти от чтения базы к RCE. Нужны привилегии FILE, но в CTF они часто есть. Проверь значение
через
Код:
UNION SELECT @@secure_file_priv
- если пустое ('') - ограничений нет; если указан путь - запись/чтение только в той директории; если NULL - FILE-операции запрещены полностью. В MySQL 8.0+ значение по умолчанию -
Код:
/var/lib/mysql-files/
.
LFI + log poisoning. Нашёл Local File Inclusion, но нет интересных файлов? Отправь запрос с PHP-кодом в User-Agent, затем подключи лог Apache через LFI:
Код:
?page=../../../var/log/apache2/access.log
. PHP-код из User-Agent выполнится при подключении лога. Грязный трюк, но работает как часы.
NoSQL injection + SSRF. На CatFlix AI помимо SSRF была ещё и NoSQL injection в эндпоинте регистрации - raw input конкатенировался в MongoDB-запрос. Сама по себе NoSQLi не давала доступ к файловой системе, но в комбинации с SSRF позволяла бы манипулировать данными для дальнейшей эскалации.
На машине HTB Guardian (по данным 0xdf) цепочка была ещё длиннее: IDOR в чат-функции → утечка credentials для Gitea → анализ исходного кода → XSS через malicious XLSX в PhpSpreadsheet → кража сессии → CSRF для создания admin-аккаунта → LFI с PHP filter chain injection → RCE. Шесть звеньев. Именно такие задачи отличают средний CTF от топового.
Практический чеклист для CTF Web категории
Мой пошаговый алгоритм, который экономит время на соревнованиях. Каждый пункт - конкретное действие, а не «подумайте о безопасности».
🔓 Часть контента скрыта: Эксклюзивный контент для зарегистрированных пользователей.
Зарегистрироваться
или
Войти
Шаг 1: Разведка (2 минуты). Открой страницу, прочитай исходный код. Запусти
Код:
ffuf -u http://target/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt -mc 200,301,302,403
. Обрати внимание на
,
,
. Нашёл
- используй
для восстановления репозитория. Проверь заголовки ответа:
,
, нестандартные cookie.
Шаг 2: Идентификация стека (1 минута). Определи язык и фреймворк. PHP → ищи
,
,
. Python Flask → проверь SSTI через
. Node.js Express → проверь прототипное загрязнение: отправь JSON вида
Код:
{"proto":{"isAdmin":true}}
и смотри, изменилось ли поведение. Также проверь SSTI, если используется шаблонизатор (Pug, EJS, Nunjucks). Java → ищи десериализацию.
Шаг 3: Поиск точек входа (5 минут). Все параметры - GET, POST, cookie, заголовки - потенциальные векторы. Перехвати запросы в Burp Suite, отправь в Repeater. Подставь одинарную кавычку
- если ответ изменился, вероятна SQLi. Подставь
- если вернулось
, это SSTI. Подставь
Код:
http://burpcollaborator.net
в параметр URL - если пришёл callback, это SSRF.
Шаг 4: Эксплуатация. Используй минимально необходимые инструменты. Для SQLi - сначала руками через Burp Repeater, потом
Код:
sqlmap -r request.txt --batch
если хочешь автоматизировать. Для SSRF - curl с кастомными заголовками. Для десериализации - генерируй payload под конкретный стек.
Шаг 5: Постэксплуатация. Получил RCE? Ищи флаг:
Код:
find / -name "flag*" 2>/dev/null
,
,
. Иногда флаг в базе данных, иногда в переменных окружения, иногда в файле за пределами webroot. Проверяй всё.
Частые ошибки и почему первые попытки проваливаются
За годы CTF я выделил паттерны, которые стабильно ломают новичков:
Попытка всё автоматизировать. Sqlmap, Nikto, dirsearch - отличные инструменты, но они не замена мозгу. На MireaCTF sqlmap не нашёл blind SQLi в cookie, а ручная проверка подтвердила уязвимость за минуту. Автоматика - усилитель, не костыль.
Игнорирование исходного кода. Если таск даёт исходники - это подсказка. Не проскакивай мимо. На Intigriti CatFlix AI ключ к решению был в трёх строчках
. Я видел людей, которые фаззили директории 40 минут, когда уязвимость лежала в открытом коде. Обидно.
Зацикливание на одном векторе. Потратил 15 минут на SQLi и ничего? Переключись. Возможно, это не SQLi, а SSTI. Или command injection. Или IDOR. Диверсифицируй проверки в первые 5 минут, потом углубляйся в самый перспективный вектор.
Непонимание формата флага. Звучит глупо, но регулярно люди находят строку, похожую на флаг, и не могут её сдать - не обернули в формат
или не декодировали из Base64. Всегда проверяй, нет ли дополнительного слоя кодирования - Deobfuscate/Decode Files or Information (T1140, Defense Evasion) работает и в обратную сторону.
Привязка к реальному пентесту
Всё, что описано выше, напрямую переносится в реальные аудиты. SQL injection в CTF - это та же SQL injection в продакшн-приложении банка. SSRF через Next.js middleware (CVE-2025-57822) - реальная CVE, которая затрагивала self-hosted приложения на Next.js до версий 14.2.32 и 15.4.7. Десериализация в PHP и Java - причина множества критических взломов, от Apache Struts до Magento.
Разница в том, что в CTF тебе дают чёткие границы и легальное разрешение. В реальном пентесте добавляется scope, согласование и отчётность. Но навык видеть уязвимость в коде за 10 минут - один и тот же.
Хочешь прокачаться в веб-категории -
решай задачи на HackerLab (секция Web), участвуй в Intigriti Monthly CTF, разбирай writeup'ы с CTFtime. И главное - пиши свои writeup'ы. Пока не объяснишь решение другому человеку, ты его не усвоил. Потренировавшись на кошках - можно и на реальный проект.