Главная / Блог / Bash скрипты для CTF: автоматизируем перебор портов, парсинг вывода и массовую обработку файлов

14 мин.00

Bash скрипты для CTF: автоматизируем перебор портов, парсинг вывода и массовую обработку файлов

Bash скрипты для CTF: автоматизируем перебор портов, парсинг вывода и массовую обработку файлов

Bash скрипты для CTF: автоматизируем перебор портов, парсинг вывода и массовую обработку файлов

На Attack-Defense CTF в прошлом сезоне нам выдали 12 хостов для атаки и 20 минут на первичную разведку. Запускать nmap поштучно — сжечь половину времени на ожидание. Цикл из шести строк Bash с фоновыми процессами через & и wait для синхронизации уложил сканирование всех 12 целей в полторы минуты. Bash не быстрее специализированных сканеров — он просто работает на любой Unix-машине с терминалом, без установки пакетов. В терминологии MITRE ATT&CK это техника Unix Shell (T1059.004, тактика Execution) — один из базовых методов выполнения команд на Unix-системах, который в CTF пронизывает всю цепочку от разведки до сбора флагов. Ниже — готовые скрипты и однострочники, закрывающие три главные рутины соревнований: перебор портов, парсинг вывода сканеров и массовую обработку файлов. Каждый разобран построчно, с описанием граблей, на которые я наступал сам.

Требования к окружению

[Применимо: CTF jeopardy, CTF attack-defense, внутренний пентест на Linux-инфраструктуре]

Прежде чем копировать скрипты — убедитесь, что окружение не подкинет сюрпризов.

  • ОС: Kali Linux, Parrot OS, Ubuntu 20.04+, macOS с оговорками. WSL2 на Windows 10/11 работает, но добавляет сетевой слой — /dev/tcp и ping могут вести себя иначе
  • Bash: версия 4.x и выше. Проверка — bash --version. На macOS из коробки стоит Bash 3.2 (лицензия GPLv3 мешает Apple обновлять), ставьте актуальную через brew install bash
  • RAM: 512 МБ достаточно для всех скриптов из статьи. При массовом фоновом переборе через & на сотни процессов — 2 ГБ минимум
  • Зависимости: curl, grep, awk, sed — есть в любом дистрибутиве из коробки. nmap и jq ставятся через пакетный менеджер: apt install nmap jq
  • Редактор: nano для начинающих, vim для тех, кто потратил час на изучение :wq. Каждый скрипт начинается с шебанга #!/bin/bash и становится исполняемым после chmod +x script.sh. Без chmod +x получите Permission denied — первая ловушка, через которую проходят все

Рабочую директорию создайте сразу: mkdir -p ~/ctf/scripts && cd ~/ctf/scripts. Все выходные файлы скриптов будут падать в одно место, и вы не потеряете результаты посреди соревнования.

Перебор портов bash: от /dev/tcp до nmap-обёртки

Работает если: Bash скомпилирован с поддержкой /dev/tcp (Kali, Ubuntu, Parrot — по умолчанию да), целевой хост доступен по TCP. Не работает если: используется dash или sh вместо bash (в Debian /bin/sh ссылается на dash, где /dev/tcp отсутствует), Bash собран без --enable-net-redirections, цель за файрволом с DROP-политикой.

Когда nmap недоступен — а на CTF-инфраструктуре с ограничениями по инструментам это случается регулярно — Bash умеет проверять TCP-порты через виртуальное устройство /dev/tcp. Это не файл на диске, а встроенный механизм оболочки: запись в /dev/tcp/host/port инициирует TCP-соединение. Порт открыт — код возврата 0, закрыт — ошибка.

#!/bin/bash
HOST="$1"
for port in $(seq 1 1024); do
  (echo >/dev/tcp/"$HOST"/"$port") 2>/dev/null && \
    echo "[+] $HOST:$port open"
done

Скрипт принимает IP первым аргументом ($1). Цикл seq 1 1024 перебирает первую тысячу портов. Конструкция echo >/dev/tcp/"$HOST"/"$port" пытается открыть TCP-соединение, скобки () создают subshell (изолируют ошибку). Перенаправление 2>/dev/null подавляет сообщения о закрытых портах. Оператор && выполняет echo только при успешном соединении.

Проблема — скрипт последовательный. На 1024 порта с таймаутом по умолчанию (около секунды на закрытый порт) уйдёт до 17 минут. На CTF это вечность. Решение — параллелизация. Добавьте & после echo в теле цикла, а после done поставьте wait. Амперсанд отправляет каждую проверку в фоновый процесс, wait дожидается завершения всех. На 1024 порта время падает до 3-5 секунд.

Но тут вторая ловушка: 1024 одновременных процесса машина переживёт, а 65535 — скорее всего нет. Ограничьте параллельность: xargs -P 50 -I{} bash -c 'echo >/dev/tcp/"$1"/{} 2>/dev/null && echo "[+] $1:{} open"' _ "$HOST" запускает максимум 50 проверок одновременно. Или GNU parallel с ключом -j 50.

Когда /dev/tcp не спасает

  • UDP-порты: /dev/tcp работает только с TCP. Для UDP нужен nmap -sU или nc -u -w 1 $HOST $port
  • Определение сервисов: чистый Bash не отправляет баннерные запросы. Вы узнаете, что порт 22 открыт, но версию OpenSSH не определите. Для версий — nmap -sV или ручной nc -w 2 $HOST $port < /dev/null
  • Таймауты на DROP-файрволах: если файрвол дропает пакеты (не отвечает RST), каждый закрытый порт висит до таймаута. В Bash нет встроенного таймаута для /dev/tcp — обернуть можно через timeout 1 bash -c "echo >/dev/tcp/$HOST/$port", но это замедляет выполнение и плодит дочерние процессы
  • Обнаружение: любой последовательный перебор портов детектируется элементарно. В SigmaHQ 19 правил для T1059.004 (Unix Shell), включая lnx_shell_susp_commands.yml. Категория Port Scan (#14 по классификации AbuseIPDB) — стандартный триггер для автоматических блокировок. На CTF это обычно не проблема, на реальном пентесте — учитывайте

Парсинг вывода команд linux: grep, awk и sed в бою

Работает если: GNU grep установлен (Kali, Ubuntu — по умолчанию), файл в текстовом формате. Не работает если: используется BSD grep без -P (macOS — заменить на grep -oE), файл бинарный (использовать strings перед grep).

Основная задача в CTF — не запустить сканер, а быстро вытащить из его вывода нужное. Русскоязычные гайды обычно заканчиваются на grep "open" — это покрывает процентов десять реальных задач парсинга. Разберём обработку вывода nmap и curl — двух инструментов, которые генерируют основной поток текстовых данных на соревнованиях.

Разбор nmap -oG через grep и awk

Ключевой флаг — -oG (grepable output). Формат nmap -oG выдаёт одну строку на хост со всеми открытыми портами — идеально для конвейерной обработки. Стандартный многострочный вывод nmap парсить неудобно — каждый порт на отдельной строке, между хостами пустые строки и декоративные рамки.

Запуск: nmap -sC -sV -oG scan.grep -oN scan.txt 10.10.0.0/24. Флаг -oG сохраняет grepable-формат, -oN — нормальный (для чтения глазами). Два формата одновременно — привычка, которая экономит время на CTF. Я так делаю всегда.

Извлечь IP-адреса с открытым портом 80: grep '80/open' scan.grep | awk '{print $2}'. Тут awk '{print $2}' выводит второе поле строки — IP-адрес (первое поле в формате -oG — слово Host:).

Получить список всех открытых портов конкретного хоста: grep '10.10.0.5' scan.grep | grep -oP '\d+/open' | cut -d'/' -f1. Регулярка \d+/open через grep -oP (Perl-совместимые регулярки) извлекает номер порта перед /open, а cut -d'/' -f1 отрезает всё после слэша. На macOS -P не поддерживается — замените на grep -oE '[0-9]+/open'.

Скрипт для массовой обработки grepable-вывода:

#!/bin/bash
# Парсит nmap -oG, выводит ip -> список портов
grep '/open/' "$1" | while read -r line; do
  ip=$(echo "$line" | awk '{print $2}')
  ports=$(echo "$line" | grep -oP '\d+/open' | \
    cut -d'/' -f1 | tr '\n' ',')
  echo "$ip -> $ports"
done

Принимает файл nmap -oG первым аргументом. Для каждой строки с открытыми портами извлекает IP через awk и формирует компактный список портов. Команда tr '\n' ',' заменяет переводы строк запятыми — получается вывод вида 10.10.0.5 -> 22,80,443,.

Несколько паттернов sed, которые на CTF пригождаются постоянно. Удаление пустых строк: sed '/^$/d' output.txt. Извлечение значения после password=: sed -n 's/.*password[=:]\s*\([^ ]*\).*/\1/p' config.txt. Разбить строку hash:salt:user на отдельные строки: echo "hash:salt:user" | sed 's/:/\n/g'. Вывод строк с 10 по 20 из лога: sed -n '10,20p' access.log.

Извлечение данных из HTTP-ответов

На CTF часто нужно обработать десятки HTTP-ответов: найти отличающийся по длине, вытащить токен из заголовка, отфильтровать ответы с определённым статусом.

Базовый паттерн — curl -s -o /dev/null -w "%{http_code} %{size_download}" — возвращает HTTP-код и размер ответа в байтах без тела. Обернув в цикл с вордлистом, получаете простой fuzzer директорий:

while read -r dir; do code=$(curl -s -o /dev/null -w "%{http_code}" "http://target/$dir"); [ "$code" != "404" ] && echo "$code $dir"; done < wordlist.txt

Квадратные скобки в [ "$code" != "404" ] — вызов команды test. Пробелы вокруг скобок и оператора обязательны. Запись ["$code" без пробела — и привет, [: command not found. Самая раздражающая синтаксическая особенность Bash и самый частый вопрос от новичков на форумах.

Для JSON-ответов — jq: curl -s http://target/api/user | jq -r '.token'. Без jq можно через grep -oP '"token":"[^"]+"' | cut -d'"' -f4, но это хрупко — регулярка сломается на вложенных кавычках. Для заголовков: curl -sI http://target | grep -i 'set-cookie' — флаг -I запрашивает только заголовки (HEAD-запрос).

Массовая обработка файлов bash: охота за флагами

Работает если: есть read-доступ к целевым директориям, GNU grep поддерживает -r. Не работает если: файловая система зашифрована, файлы в нестандартной кодировке (UTF-16 — grep не увидит ASCII-строки без предварительной конвертации через iconv).

В MITRE ATT&CK это пересечение двух техник: File and Directory Discovery (T1083, тактика Discovery) — перечисление файлов через find и ls — и Automated Collection (T1119, тактика Collection) — автоматизированный сбор данных из найденных файлов. В CTF-контексте: вы получили shell и ищете флаг, или скачали дамп файловой системы и обрабатываете его локально.

Рекурсивный поиск по файловой системе

Классический однострочник из CTF-чит-шитов (HackTricks, PayloadsAllTheThings): grep -rE 'flag\{|CTF\{|HTB\{|THM\{' / 2>/dev/null. Флаг -r — рекурсивный поиск, -E — расширенные регулярки, 2>/dev/null — подавление ошибок Permission denied. Проблема: на живой системе с тысячами файлов это медленно и генерирует шум в логах.

Оптимизированный вариант — ограничить область поиска типичными CTF-директориями:

#!/bin/bash
# Поиск флагов в типичных CTF-директориях
DIRS="/root /home /opt /var/www /tmp /srv"
PATTERN='flag\{|CTF\{|HTB\{|THM\{|picoCTF\{'
for d in $DIRS; do
  [ -d "$d" ] && grep -rlE "$PATTERN" "$d" 2>/dev/null
done

Проверка [ -d "$d" ] исключает несуществующие пути (без неё grep отработает, но выдаст ошибку). Флаг -l выводит только имена файлов с совпадениями, без самих строк — быстрее и компактнее. Переменная DIRS — типичные места, где CTF-платформы прячут флаги: домашние директории пользователей, /opt (кастомные приложения), /var/www (веб-корень), /tmp (временные файлы).

Для бинарных файлов grep по умолчанию выводит Binary file matches. Добавьте флаг -a (обрабатывать как текст) или лучше: strings "$file" | grep -E "$PATTERN" — утилита strings извлекает печатные последовательности из бинарников, а дальше grep работает с чистым текстом. Команда find / -type f -exec file {} + | grep 'ELF' найдёт все ELF-бинари, а strings с grep обработает каждый.

Batch-обработка дампов и архивов

На forensics-задачах часто попадается директория с сотнями файлов неизвестного типа: дампы, архивы, образы. Задача — обработать каждый автоматически.

Паттерн с find и -exec: find ./dump -type f -name '*.gz' -exec gunzip {} \; распаковывает все .gz-файлы. Но на CTF расширения регулярно врут — файл image.png может оказаться ZIP-архивом. Доверяй magic bytes, а не расширению:

find ./dump -type f | while read -r f; do file "$f" | grep -q 'gzip' && gunzip "$f"; done

Команда file "$f" определяет тип по magic bytes (первые байты файла), grep -q 'gzip' проверяет результат, и только тогда запускается распаковка.

Критическая ловушка: имена файлов с пробелами. Конструкция while read -r f разбивает ввод по умолчанию по пробелам и переводам строк. Файл my flag.txt превратится в два аргумента: my и flag.txt. Решение: find ... -print0 | while IFS= read -r -d '' f — разделитель нулевой байт вместо перевода строки. Это одна из самых коварных ошибок — скрипт молча пропускает файлы с пробелами, и вы теряете флаг, не зная об этом. Я на одном CTF потерял полчаса именно на этом.

Для определения распределения типов файлов в директории: find ./dump -type f -exec file {} + | awk -F: '{print $2}' | sort | uniq -c | sort -rn. Показывает, сколько PNG, ELF, text, gzip — и помогает понять, за что хвататься первым.

Bash one-liner для хакинга: рабочие однострочники

Однострочники — рабочий инструмент CTF-цейтнота. Каждый решает конкретную задачу и помещается в одну строку терминала. Скопировал, подставил цель, запустил.

Поиск SUID-бинарей для эскалации привилегий: find / -perm -4000 -type f 2>/dev/null. Флаг -perm -4000 ищет файлы с установленным SUID-битом. Результат проверяется по GTFOBins (gtfobins.github.io) — каталогу Unix-бинарей для злоупотребления при post-exploitation. Из частых находок: awk, bash, base64, cat — все могут читать файлы или давать shell при наличии SUID.

Разведка системы одной командой (T1082, System Information Discovery): echo "=== OS ==="; uname -a; echo "=== Users ==="; cat /etc/passwd | grep -v nologin; echo "=== Net ==="; ip a; echo "=== Procs ==="; ps aux --forest. Шесть команд в одной строке — вместо шести отдельных запусков.

Перебор поддоменов: while read -r sub; do host "$sub.target.com" | grep -v 'NXDOMAIN' && echo "[+] $sub"; done < subdomains.txt. Команда host резолвит DNS, grep -v 'NXDOMAIN' отфильтровывает несуществующие.

Генерация числового вордлиста: for i in $(seq -w 0000 9999); do echo $i; done > pins.txt. Флаг -w в seq добавляет ведущие нули — без него 0001 генерируется как 1, и четырёхзначный PIN не совпадёт с форматом.

Мониторинг изменений страницы: watch -n 5 'curl -s http://target/score | md5sum'. Показывает MD5-хеш страницы каждые 5 секунд — хеш изменился, значит содержимое обновилось.

Декодирование Base64: cat encoded.txt | base64 -d. Для URL-safe Base64 (где + заменён на -, а / на _): cat encoded.txt | tr '_-' '/+' | base64 -d.

Написание скриптов для сканирования сети: wrapper-паттерн

Автоматизация CTF на практике — не монолитный скрипт на 200 строк, а набор wrapper'ов. Каждый оборачивает конкретный инструмент и передаёт результат следующему. Паттерн: входные данные — из аргументов или файла, результат — в файл для следующего скрипта.

#!/bin/bash
set -euo pipefail
TARGET="$1"
OUT="./results/$TARGET"
mkdir -p "$OUT"
echo "[$(date +%H:%M:%S)] Scanning $TARGET..."
nmap -sC -sV -oG "$OUT/nmap.grep" -oN "$OUT/nmap.txt" "$TARGET"
echo "[$(date +%H:%M:%S)] Extracting ports..."
grep '/open/' "$OUT/nmap.grep" | grep -oP '\d+/open' | \
  cut -d'/' -f1 > "$OUT/ports.txt"
echo "[*] Found $(wc -l < "$OUT/ports.txt") open ports"

Скрипт создаёт директорию результатов для каждого хоста, запускает nmap с двумя форматами вывода, извлекает открытые порты в отдельный файл. Команда wc -l < "$OUT/ports.txt" считает строки — перенаправление через < вместо cat file | wc -l избегает вывода имени файла в результате.

Строка set -euo pipefail — три защитных флага, и каждый спасал мне скрипты не раз. set -e — прерывание при первой ошибке (без него скрипт продолжит после упавшего nmap и следующий шаг получит пустой файл). set -u — ошибка при обращении к неинициализированной переменной (опечатка $TAGET вместо $TARGET молча подставит пустую строку, и nmap просканирует localhost — приятный сюрприз). set -o pipefail — конвейер возвращает код ошибки последней упавшей команды, а не последней в цепочке.

Дальше подключаются модульные скрипты: web_enum.sh запускает gobuster dir по HTTP-портам из ports.txt, grab_banners.sh собирает баннеры через nc. Каждый читает файл из директории результатов и пишет туда же. Проверка аргументов в начале каждого скрипта: [ -z "$1" ] && echo "Usage: $0 <target>" && exit 1 — без этого скрипт запустится с пустой переменной и выдаст непонятную ошибку на третьей строке.

Временные метки через echo "[$(date +%H:%M:%S)]" — не декорация. На CTF с ограниченным временем они помогают понять, на каком шаге скрипт застрял, если процесс завис. Разница между «скрипт работает» и «скрипт завис на nmap уже 8 минут» — одна строчка с date.

Когда bash скрипты для пентеста недостаточно

Bash — клей между утилитами, не полноценный язык программирования. И знание его границ экономит часы.

Многопоточность с контролем. Фоновые процессы через & — грубый механизм. Нет пула потоков, нет очереди задач. Для перебора 65535 портов с лимитом в 50 одновременных соединений — xargs -P 50 или GNU parallel, но логика быстро превращается в кашу. В Python concurrent.futures.ThreadPoolExecutor решает это в три строки с полным контролем.

Работа с JSON/XML. jq покрывает простой JSON, но парсинг вложенных структур через цепочки grep | awk | sed — путь к хрупкому коду, который ломается при каждом изменении формата. REST API современных CTF-платформ возвращают JSON — обрабатывать их на Bash можно, но больно. Python с json и requests — надёжнее.

Сложная логика с состоянием. HTTP-сессия (cookies, CSRF-токены), повторы при ошибках, цепочки редиректов — количество строк Bash растёт экспоненциально. requests.Session() в Python делает это элегантнее.

Криптография. Crypto-задачи (XOR, AES, RSA) через openssl и xxd в Bash — можно, но непрактично для чего-то сложнее Base64. Python с pycryptodome — стандарт для crypto на CTF.

Правило для принятия решения: скрипт перерос 30 строк и содержит больше трёх вложенных if/while — переписывайте на Python. Bash хорош для pipeline из 5-15 строк, где каждая строка — вызов утилиты через конвейер. Всё сложнее — не его территория.

С точки зрения обнаружения: защитные системы отслеживают подозрительную shell-активность. В SigmaHQ 19 правил детекции для T1059.004 (Unix Shell), включая lnx_shell_susp_rev_shells.yml для обнаружения reverse shell и lnx_shell_susp_commands.yml для подозрительных команд. Контрмеры по D3FEND включают Executable Denylisting (D3-EDL) — блокировку исполнения по белым спискам — и Content Filtering (D3-CF). На CTF эти механизмы обычно выключены, на реальном пентесте Bash-скрипт может быть заблокирован EDR до первого выполнения.

На каждом соревновании вижу одну и ту же картину: команды делятся на тех, кто пишет скрипты под задачу за минуты, и тех, кто тратит эти минуты на ручной ввод. Разница в итоговом счёте — не в знании уязвимостей, а в скорости обработки информации. Bash покрывает 80% рутины на CTF: перебор портов, парсинг вывода, поиск флагов по файловой системе. Оставшиеся 20% — сложная логика, крипто, работа с API — уходят Python. Но навык собирать конвейер из grep | awk | sort | uniq за 30 секунд, пока соперник читает мануал — не появляется от чтения статей. Он появляется после 50-й задачи, когда начинаешь думать конвейерами. После 200-й — видишь паттерны в выводе до того, как запустил grep. Если идёшь к OSCP и нужна подготовка по разведке и скриптингу — WAPT покрывает это в первых модулях с лабами на каждый кейс.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...