
На PicoCTF попалась задача: форма авторизации с четырёхзначным PIN-кодом, никакого rate limiting, никаких CSRF-токенов. 10 000 комбинаций, руками не вариант. Python на машине не стоял, ставить что-то через pip — ограничения по сети. Решение уложилось в семь строк Bash и 40 секунд работы скрипта. Вот и вся суть Bash-скриптинга в CTF: не замена ffuf или hydra, а навык, который работает на любой Unix-машине, где есть терминал. Ниже — три скрипта, которые закрывают типовые задачи CTF начального уровня: брутфорс формы, перебор параметров и парсинг вывода. Каждый разобран построчно, с разбором ошибок, которые я сам допускал и видел у десятков новичков.
Bash в терминологии MITRE ATT&CK — техника T1059.004 (Unix Shell, тактика Execution). Один из базовых методов выполнения команд на Unix-системах. В CTF он появляется не на одном этапе, а пронизывает всю цепочку. Подробнее — в нашем подробном разборе linux для пентестера.
Разведка (Discovery). Автоматический сбор информации о системе: перечисление файлов через find и ls (T1083, File and Directory Discovery), сбор системных данных через uname, id, cat /etc/os-release (T1082, System Information Discovery). Bash-скрипт позволяет запустить десяток разведывательных команд одним вызовом и скинуть вывод в файл.
Credential Access. Брутфорс паролей и PIN-кодов через HTTP-формы или SSH — техники T1110.001 (Password Guessing) и T1110.003 (Password Spraying). Цикл с curl или обёртка над hydra решает задачу без дополнительных зависимостей.
Collection. Автоматизированный сбор данных из найденных файлов — T1119 (Automated Collection). Парсинг результатов nmap, фильтрация логов, поиск флагов по файловой системе через grep -rE 'flag\{|CTF\{|HTB\{' / 2>/dev/null — однострочник, который в CTF используется повсеместно.
Тут главное понять: Bash — не конечный инструмент атаки, а клей между утилитами. Он склеивает nmap, curl, grep, hydra в автоматизированные цепочки. В CTF это критично — время ограничено, и ручной ввод съедает минуты, которые отделяют решение от поражения.
| Критерий | Bash-скрипт | Специализированный инструмент (ffuf, hydra, gobuster) |
|---|---|---|
| Преимущества | Доступен на любой Unix-системе без установки, полный контроль над логикой, нет зависимостей | Многопоточность из коробки, встроенная обработка ошибок HTTP, оптимизация под задачу |
| Ограничения | Однопоточный по умолчанию, громоздкая обработка cookies и редиректов, нет встроенного rate limiting | Требует установки, менее гибкий для нестандартных протоколов и форматов |
| Когда использовать | Нестандартная логика запроса, ограниченное окружение, быстрый прототип до 50 строк | Массовый перебор от 10 000+ вариантов, стандартные протоколы (HTTP, SSH, FTP) |
| Когда НЕ использовать | Перебор от 50 000 вариантов, сложная работа с сессиями и JSON, параллелизм от 20+ потоков | Уникальный протокол, нестандартный формат ответа, отсутствие Go/Python на целевой машине |
Прежде чем писать первый скрипт — проверьте готовность окружения.
brew install bash. WSL2 на Windows 10/11 подойдёт, но добавляет слой сетевых абстракцийbash --version. Для скриптов из этой статьи хватит любой 4.x+curl (HTTP-запросы), grep, sed, awk (парсинг текста), nmap (сканирование портов). В Kali всё есть из коробки. На минимальных серверных дистрибутивах nmap и curl ставятся через пакетный менеджер: apt install nmap curlnano для начинающих, vim для тех, кто готов вложить час в изучение базовых командСоздайте рабочую директорию: mkdir ~/ctf && cd ~/ctf. Каждый скрипт начинается с шебанга #!/bin/bash — эта строка указывает системе, какой интерпретатор использовать. Без неё скрипт может запуститься в sh, dash или другой оболочке с отличающимся синтаксисом. После создания файла сделайте его исполняемым: chmod +x script.sh. Без этого получите Permission denied — первая ловушка для новичков, через которую проходят все.
Самый частый сценарий в CTF для новичков — веб-форма с паролем из вордлиста или числовым PIN. Задача: отправить POST-запрос с каждым вариантом пароля и найти ответ, отличающийся от стандартного «Access denied».
Перед написанием скрипта разберите задачу руками. Откройте форму в браузере, введите заведомо неверный пароль и загляните в Developer Tools (вкладка Network): какой URL принимает форму, какой метод используется (POST или GET), как называются поля ввода (обычно username и password), какой текст возвращает сервер при ошибке. Эти четыре параметра — входные данные для скрипта.
Допустим, форма отправляет POST на http://target:8080/login с полями user=admin&pass=VALUE, а при неверном пароле возвращает строку «Access denied».
#!/bin/bash
TARGET="http://target:8080/login"
while read -r pass; do
resp=$(curl -s -d "user=admin&pass=${pass}" "$TARGET")
if ! echo "$resp" | grep -q "Access denied"; then
echo "[+] Найден пароль: $pass"
break
fi
done < wordlist.txt
Построчный разбор. Конструкция while read -r pass читает файл wordlist.txt строка за строкой, помещая каждую строку в переменную pass. Флаг -r тут критически важен — он запрещает Bash интерпретировать обратные слэши. Без -r пароль вида test\n123 превратится в testn123, и вы никогда не найдёте совпадение. Подвох: < wordlist.txt стоит в конце строки done, а не в начале цикла — синтаксическая особенность Bash, к которой просто надо привыкнуть.
Команда curl -s -d "user=admin&pass=${pass}" "$TARGET" отправляет POST-запрос. Флаг -s подавляет прогресс-бар (без него вывод скрипта замусоривается). Флаг -d задаёт тело запроса. Результат через подстановку $() попадает в переменную resp.
Проверка echo "$resp" | grep -q "Access denied" ищет в ответе строку ошибки. Флаг -q переводит grep в тихий режим — он ничего не выводит, а только устанавливает код возврата: 0 при совпадении, 1 при отсутствии. Оператор ! инвертирует условие: если строки «Access denied» нет — пароль подошёл. Команда break прерывает цикл.
Типичная ошибка: одинарные кавычки вместо двойных в строке curl. В одинарных кавычках ${pass} не раскрывается и улетает на сервер буквально как текст ${pass}. Двойные кавычки обязательны для подстановки переменных.
Вариант для числового PIN. Для перебора кодов 0000-9999 замените конструкцию while read на цикл for pin in $(seq -w 0000 9999); do и подставляйте $pin вместо $pass. Флаг -w в команде seq добавляет ведущие нули — без него генерируется 1 вместо 0001, и четырёхзначный PIN не совпадёт с ожидаемым форматом.
Этот подход работает, когда вариантов немного (до 10 000-50 000) и сервер не ограничивает частоту запросов. Вот против чего скрипт бесполезен:
sleep 0.5 перед done, но это растягивает перебор 10 000 вариантов с 40 секунд до 83 минутcurl для получения страницы с токеном и grep для его извлечения — но скрипт разрастается до 20+ строк и становится хрупкимselenium в PythonГде это применимо: CTF-задачи начального и среднего уровня, legacy-веб-приложения без WAF на внутреннем пентесте. На реальных проектах с Cloudflare, fail2ban или ModSecurity чистый Bash-брутфорс не пройдёт — используйте hydra для стандартных протоколов (SSH, FTP, HTTP Basic Auth) или Python с requests.Session() для сложной логики.
Вторая классическая задача — найти скрытую директорию, файл или GET-параметр, меняющий поведение приложения. ffuf или gobuster справляются быстрее за счёт многопоточности, но понимание механики начинается с ручной реализации.
Перебор директорий по вордлисту строится на проверке HTTP-кода ответа. Однострочник curl -o /dev/null -s -w "%{http_code}" http://target/dir/ возвращает только код состояния: 200 (страница существует), 301 (редирект — директория есть, но перенаправляет), 404 (не найдено). Флаг -o /dev/null сбрасывает тело ответа, -w "%{http_code}" выводит статус. Оберните это в цикл while read -r dir — и получите простой сканер директорий.
Для перебора GET-параметров подход другой — ищете не код ответа, а разницу в длине ответа. Если параметр влияет на вывод, длина ответа при его наличии отличается от базовой.
#!/bin/bash
TARGET="http://target:8080/page"
BASELINE=$(curl -s "$TARGET" | wc -c)
while read -r param; do
LEN=$(curl -s "${TARGET}?${param}=test" | wc -c)
if [ "$LEN" -ne "$BASELINE" ]; then
echo "[+] Параметр: $param (длина: $LEN vs $BASELINE)"
fi
done < params.txt
Команда wc -c считает количество байт в ответе. Сначала замеряем «базовую» длину страницы без параметров, затем сравниваем с длиной при каждом подставленном параметре из файла params.txt. Разница означает: сервер обработал параметр и изменил вывод. Это не доказательство уязвимости, но точка входа для дальнейшего ковыряния.
Ловушка с квадратными скобками. Конструкция [ "$LEN" -ne "$BASELINE" ] — это вызов команды test (квадратная скобка — её алиас). Пробелы вокруг скобок и оператора обязательны. Запись ["$LEN" без пробела вызывает ошибку [: command not found. Запись [ "$LEN"-ne"$BASELINE" ] без пробелов вокруг -ne воспринимается как одна строка, а не три аргумента, и тоже ломается. Самая раздражающая особенность Bash — каждый пробел имеет значение.
По умолчанию Bash выполняет команды последовательно: отправил запрос, дождался ответа, обработал, отправил следующий. При 10 000 директорий и среднем времени ответа 100 мс это 16 минут чистого ожидания. Простейшее ускорение — добавить & после команды curl, чтобы каждый запрос ушёл в фоновый процесс, и wait после цикла для ожидания завершения. Но без контроля количества параллельных процессов можно положить и свою машину (тысячи одновременных процессов), и целевой сервер. Ограничитель: добавьте перед каждым фоновым запуском проверку while [ $(jobs -r | wc -l) -ge 10 ]; do sleep 0.1; done — это удержит параллельность в пределах 10 процессов.
Параллельный Bash работает, но хрупок: вывод разных процессов перемешивается, обработка ошибок усложняется, отладка превращается в мучение. Если задача требует больше 10-20 параллельных запросов — берите ffuf (написан на Go, однофайловый бинарь без зависимостей) или gobuster. Они заточены под массовый перебор и справляются с 200+ потоками без проблем.
Третий навык, без которого автоматизация CTF на Bash бесполезна — извлечение нужных данных из вывода команд. Связка grep + awk + sed покрывает подавляющее большинство задач парсинга.
grep — поиск строк по паттерну. Ключевые флаги для CTF: -i (регистронезависимый поиск — удобно для флагов в разном регистре), -r (рекурсивный поиск по директориям), -E (расширенные регулярные выражения с поддержкой | для альтернатив), -o (вывод только совпавшей части строки, а не всей строки). Команда grep -rEo 'flag\{[^}]+\}' /home/ найдёт все вхождения формата flag{...} во всех файлах директории /home/ и выведет только сами флаги.
awk — извлечение полей из структурированного вывода. Результат nmap содержит строки вида 22/tcp open ssh OpenSSH 8.9p1. Чтобы вытащить номер порта: awk -F/ '{print $1}' — флаг -F/ задаёт слэш как разделитель полей, $1 берёт первое поле (число до слэша). Чтобы вытащить название сервиса: awk '{print $3}' — по умолчанию awk разделяет по пробелам, третье поле — имя сервиса.
sed — потоковая замена текста. Команда sed 's/\/tcp//' удалит подстроку /tcp из номера порта. Синтаксис s/что/на_что/ — подстановка. Слэш в искомой строке экранируется обратным слэшем (\/), что делает запись визуально шумной. Альтернатива — другой разделитель: sed 's|/tcp||' делает то же самое, но читается чище.
Типичная цепочка для CTF: просканировать цель, вытащить открытые порты, передать их в детальное сканирование.
#!/bin/bash
TARGET=$1
nmap -sV -oN scan.txt "$TARGET"
PORTS=$(grep "open" scan.txt | awk -F/ '{print $1}' | tr '\n' ',')
echo "[*] Открытые порты: $PORTS"
nmap -sC -p "$PORTS" "$TARGET" -oN detailed.txt
Переменная $1 — первый аргумент командной строки. Скрипт запускается как ./scan.sh 10.10.10.1. Первый nmap -sV делает быстрое сканирование с определением версий сервисов. Результат сохраняется в scan.txt через -oN (формат normal output). Затем grep "open" отбирает строки с открытыми портами, awk -F/ вытаскивает номера, tr '\n' ',' заменяет переносы строк на запятые — получается строка вида 22,80,443,. Эта строка передаётся во второй nmap -sC (запуск скриптов по умолчанию) для детального исследования только найденных портов. Двухэтапное сканирование экономит время: полное сканирование 65 535 портов со скриптами занимает десятки минут, а сканирование трёх найденных — секунды.
Ошибка с кавычками вокруг переменных. Запись nmap -sV $TARGET без кавычек работает для IP-адреса, но ломается, если аргумент содержит пробелы или спецсимволы. Привычка заключать каждую переменную в двойные кавычки — "$TARGET" — спасает от класса ошибок, которые потом часами отлаживаешь.
Извлечение данных из истории команд. В CTF на OverTheWire Bandit и аналогичных wargames флаги иногда прячут в истории команд предыдущих пользователей — техника T1552.003 (Shell History). Команда cat ~/.bash_history | grep -i password ищет упоминания паролей в истории. На серверах с несколькими пользователями стоит проверить cat /home/*/.bash_history 2>/dev/null — перенаправление ошибок доступа в /dev/null подавит сообщения о директориях без прав чтения.
Bash достаточен для CTF-задач с линейной логикой: перебрать список, проверить условие, вывести результат. Граница проходит по трём маркерам.
Сложная обработка HTTP. Когда нужно парсить JSON-ответ, поддерживать сессию между запросами (cookie jar) или обрабатывать цепочку редиректов с извлечением заголовков — curl в цикле становится неуправляемым. Python с requests и session.post() решает задачу в пять строк с автоматическим управлением cookies.
Криптографические операции. Декодирование base64 в Bash работает — echo "data" | base64 -d. Но XOR-шифрование, генерация хэшей с солью, работа с RSA — территория Python. В CTF категории crypto без Python или аналога не обойтись.
Структурированные данные. Парсинг JSON через grep/awk возможен для простых случаев, но хрупок: один лишний пробел или перенос строки в JSON — и скрипт выдаёт мусор. Python с json.loads() устойчив к форматированию и обрабатывает вложенные структуры.
Исследование на arxiv показало, что GPT-4o при автономном решении CTF-задач OverTheWire Bandit сгенерировал корректную однострочную bash-команду с первой попытки в 80% случаев — 18 из 25 уровней решены без подсказок. Провалы случились на задачах, требующих многошагового взаимодействия с сохранением состояния: переменных между командами, рабочих директорий между вызовами, взаимодействия с демонами. Это ровно та граница, которую чувствует и живой человек: пока задача укладывается в одну цепочку пайпов — Bash справляется. Появляется состояние между шагами — сессия, cookie, промежуточные вычисления — пора переключаться.
Практическое правило: если Bash-скрипт перевалил за 30 строк и содержит больше трёх вложенных if-блоков — перепишите на Python. Время отладки сложного Bash-скрипта с экранированием кавычек, обработкой пробелов в переменных и парсингом вложенных структур всегда превышает время полного переписывания на Python с нуля. Проверено неоднократно.
Большинство материалов по Bash заканчиваются на echo "Hello World" и конструкции if-then-else. В CTF от этого толку мало — там нужен скрипт, который за полминуты перебирает тысячи PIN-кодов или за минуту парсит результаты сканирования подсети. Разница между «знаю синтаксис» и «решаю задачи» — три конкретных навыка: цикл с curl для брутфорса, подстановка переменных для фаззинга, цепочка grep-awk-sed для парсинга. Я постоянно вижу, как люди, прочитавшие учебник по Bash от корки до корки, тратят по 15 минут на ручной ввод однотипных команд в CTF — и не успевают дойти до флага. Проблема не в знании синтаксиса, а в отсутствии привычки автоматизировать. Скрипты из статьи — не эталон, а шаблоны. Скопируйте, измените URL и имена полей под задачу, запустите, посмотрите на ошибку. Первый скрипт ломается всегда — и отладка учит глубже, чем любой учебник. Если хочется пройти путь от «не понимаю Bash» до первых решённых задач системно — на IB Basics на codeby.school эту базу закрывают за пару месяцев, от терминала до первых практических кейсов.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...