Веб - самая популярная и самая непредсказуемая категория в CTF. Тут нет формулы «запустил скрипт - получил флаг». Каждый таск - мини-приложение со своей логикой, и побеждает тот, кто быстрее находит отклонение от нормы. За последние пару лет я прошёл несколько сотен веб-тасков на HackTheBox, PicoCTF и Intigriti Monthly Challenges, и вижу три устойчивых столпа: SQL injection, SSRF и insecure deserialization. Обёртки меняются, ядро эксплуатации - нет.
В этом CTF web writeup разберу конкретные подходы с реальных соревнований - с объяснением, почему первые попытки обычно проваливаются и что с этим делать.
Методология разбора веб-тасков в CTF
Прежде чем лезть в конкретные уязвимости, нужна система. Без неё ты потратишь час на фаззинг директорий, когда ответ лежит в комментарии к HTML. Вот мой порядок действий на любом веб-таске:Первые 60 секунд - открываю сайт в браузере, смотрю исходный код страницы (Ctrl+U), проверяю заголовки ответа через DevTools. Часто прямо в комментариях или в заголовке
X-Powered-By уже видно стек: PHP, Flask, Express. Это сразу сужает вектор. На SANReN CTF, например, рекомендуют пробовать обращаться к index.php, index.cgi, index.html - по ответу сервера понятно, что под капотом.Следующие 5 минут - если есть исходный код (а в современных CTF его всё чаще дают), читаю серверную часть. Не весь код, а точки входа: роутеры, обработчики POST-запросов, middleware. Именно тут прячутся инъекции. Если исходников нет - запускаю
ffuf -u http://target/FUZZ -w common.txt для поиска скрытых эндпоинтов, включая .git/HEAD, /backup, /admin.Когда нашёл потенциальный вектор - проверяю вручную через Burp Suite, а не сразу бросаю автоматику. На MireaCTF, по данным одного из writeup'ов с Codeby, sqlmap не смог найти blind SQL injection в cookie, хотя уязвимость подтвердилась вручную за минуту. Автоматика хороша, но она не понимает кастомную логику конкретного таска.
В терминах MITRE ATT&CK весь этот процесс - Exploit Public-Facing Application (T1190, Initial Access). Но в CTF мы обычно идём дальше: после первичной эксплуатации нужно прочитать файл, достучаться до внутреннего сервиса или поднять привилегии.
SQL Injection в CTF задачах
SQLi остаётся самой частой уязвимостью в веб-категории CTF. Но если ты думаешь, что это про' OR 1=1 -- - значит давно не играл. Современные таски строятся вокруг обходов фильтров, нестандартных точек инъекции и ситуаций, где автоматика бессильна.UNION-based: когда видишь вывод
Классический сценарий: приложение выводит данные из базы на страницу, и ты можешь «подклеить» свой запрос через UNION SELECT. Первое, что нужно определить - количество колонок. Я делаю это черезORDER BY N, увеличивая N, пока не получу ошибку. Нашёл, что колонок 3? Отлично: ' UNION SELECT 1,2,3-- - и смотришь, какая цифра отобразилась на странице. Та позиция - твоё «окно» для извлечения данных.Типичная ошибка новичков - пытаться сразу читать таблицу
users. В 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. Полезны флаги --string="Welcome" (текст при true) и --not-string="Error". Но если и это не помогает - пиши свой скрипт на Python с requests, бинарным поиском по 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, исходный код приложен. Вот как шёл ход мыслей, когда я разбирал аналогичную задачу.Первое, что бросается в глаза в
middleware.ts - код добавляет security headers к ответу. Но есть блок, который проверяет UTM-параметры в запросе. Когда utm_source присутствует, NextResponse.next() вызывается без явной передачи объекта request - и пользовательские заголовки пробрасываются на сервер некорректно. Передача заголовка Location вызывает серверный редирект к произвольному 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 - когда
next() вызывается без передачи request object. В реальных условиях CVSS 6.5 (MEDIUM) отражает именно это ограничение: при пентесте нужно сначала подтвердить наличие уязвимой конфигурации. В CTF авторы задачи это условие гарантируют - спасибо им за это.Proof of concept элементарный: отправляешь GET-запрос с
?utm_source=meta и заголовком Location: http://localhost:3000/, и вместо нормального ответа получаешь содержимое внутреннего сервиса. По данным writeup'а с Intigriti, далее через перебор портов (ffuf по диапазону) был найден 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)
Обход фильтров localhost в CTF
Если авторы таска не полные новички, прямойhttp://localhost будет заблокирован. Вот техники обхода, которые работали у меня на реальных соревнованиях:DNS rebinding и альтернативные представления IP. Вместо
127.0.0.1 пробуй http://127.1, http://0x7f000001, http://2130706433 (decimal), http://[::1] (IPv6). На MireaCTF один из тасков отдавал флаг при обращении к http://0:80/flag - фильтр проверял строку «localhost» и «127.0.0.1», но не альтернативные записи. На заборе тоже написано «вход запрещён».Редиректы через внешний сервер. Поднимаешь свой сервер, который возвращает 302 на
http://127.0.0.1:порт. Приложение проверяет URL до запроса, видит твой домен - всё чисто. Но HTTP-клиент следует за редиректом и попадает на localhost. Для быстрого решения на CTF подходит ngrok или одноразовый Flask-сервер.URL-схемы. Если SSRF реализован через
curl или аналог, пробуй file:///etc/passwd для чтения локальных файлов. Или gopher:// для отправки произвольных 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 десериализация черезunserialize() опасна из-за магических методов. При вызове unserialize() PHP дёргает [B]wakeup() (или [/B]unserialize() в PHP 8.0+, который имеет приоритет). Когда объект уничтожается - __destruct(). Если в любом из этих методов есть обращение к файловой системе, выполнение команд или запись данных - это потенциальный RCE.Моя методика на CTF: нахожу вызов
unserialize() в исходниках, затем ищу все классы с [B]destruct, [/B]wakeup, [B]toString, [/B]call. Строю «gadget chain» - цепочку объектов, где свойство одного является объектом другого класса, и при вызове магического метода срабатывает нужная мне логика. Типовой пример: класс Logger с __destruct(), который записывает $this->logFile с содержимым $this->logData. Подменяем logFile на путь к PHP-файлу, logData - на вебшелл, сериализуем, отправляем - и получаем Web Shell (T1505.003, Persistence по MITRE ATT&CK). Красота.Ещё один важный момент - формат
phar://. Даже если прямого вызова unserialize() нет, но есть файловая операция (file_exists(), fopen(), is_dir()), можно загрузить PHAR-архив с сериализованным объектом в метаданных. При обращении к phar://uploads/evil.jpg PHP автоматически десериализует метаданные - сам, без спроса. Эта техника использовалась, по данным коллекции best-web-ctf-writeups на GitHub, в задаче PDF Creator с CCCamp 2019.Java: ysoserial и реальность
Java-десериализация - тяжёлая артиллерия. Если приложение принимает сериализованные Java-объекты (маркерAC ED 00 05 в hex или rO0AB в Base64), это почти гарантированный RCE при наличии уязвимых библиотек в classpath.Инструмент
ysoserial генерирует 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.Но не всё так просто - нужно угадать, какая библиотека есть на сервере. Если исходники доступны, смотрю
pom.xml или build.gradle на наличие 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 они часто есть. Проверь значение @@secure_file_priv через 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. Обрати внимание на .git/HEAD, /robots.txt, /backup. Нашёл .git - используй git-dumper для восстановления репозитория. Проверь заголовки ответа: Server, X-Powered-By, нестандартные cookie.Шаг 2: Идентификация стека (1 минута). Определи язык и фреймворк. PHP → ищи
unserialize(), include(), eval(). Python Flask → проверь SSTI через {{7*7}}. Node.js Express → проверь прототипное загрязнение: отправь JSON вида {"[B]proto[/B]":{"isAdmin":true}} и смотри, изменилось ли поведение. Также проверь SSTI, если используется шаблонизатор (Pug, EJS, Nunjucks). Java → ищи десериализацию.Шаг 3: Поиск точек входа (5 минут). Все параметры - GET, POST, cookie, заголовки - потенциальные векторы. Перехвати запросы в Burp Suite, отправь в Repeater. Подставь одинарную кавычку
' - если ответ изменился, вероятна SQLi. Подставь {{7*7}} - если вернулось 49, это 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, cat /flag.txt, env | grep FLAG. Иногда флаг в базе данных, иногда в переменных окружения, иногда в файле за пределами webroot. Проверяй всё.
Частые ошибки и почему первые попытки проваливаются
За годы CTF я выделил паттерны, которые стабильно ломают новичков:Попытка всё автоматизировать. Sqlmap, Nikto, dirsearch - отличные инструменты, но они не замена мозгу. На MireaCTF sqlmap не нашёл blind SQLi в cookie, а ручная проверка подтвердила уязвимость за минуту. Автоматика - усилитель, не костыль.
Игнорирование исходного кода. Если таск даёт исходники - это подсказка. Не проскакивай мимо. На Intigriti CatFlix AI ключ к решению был в трёх строчках
middleware.ts. Я видел людей, которые фаззили директории 40 минут, когда уязвимость лежала в открытом коде. Обидно.Зацикливание на одном векторе. Потратил 15 минут на SQLi и ничего? Переключись. Возможно, это не SQLi, а SSTI. Или command injection. Или IDOR. Диверсифицируй проверки в первые 5 минут, потом углубляйся в самый перспективный вектор.
Непонимание формата флага. Звучит глупо, но регулярно люди находят строку, похожую на флаг, и не могут её сдать - не обернули в формат
CTF{...} или не декодировали из 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'ы. Пока не объяснишь решение другому человеку, ты его не усвоил. Потренировавшись на кошках - можно и на реальный проект.
Последнее редактирование модератором: