
На 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-инфраструктуре]
Прежде чем копировать скрипты — убедитесь, что окружение не подкинет сюрпризов.
/dev/tcp и ping могут вести себя иначеbash --version. На macOS из коробки стоит Bash 3.2 (лицензия GPLv3 мешает Apple обновлять), ставьте актуальную через brew install bash& на сотни процессов — 2 ГБ минимумcurl, grep, awk, sed — есть в любом дистрибутиве из коробки. nmap и jq ставятся через пакетный менеджер: apt install nmap jqnano для начинающих, vim для тех, кто потратил час на изучение :wq. Каждый скрипт начинается с шебанга #!/bin/bash и становится исполняемым после chmod +x script.sh. Без chmod +x получите Permission denied — первая ловушка, через которую проходят всеРабочую директорию создайте сразу: mkdir -p ~/ctf/scripts && cd ~/ctf/scripts. Все выходные файлы скриптов будут падать в одно место, и вы не потеряете результаты посреди соревнования.
Работает если: 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 работает только с TCP. Для UDP нужен nmap -sU или nc -u -w 1 $HOST $portnmap -sV или ручной nc -w 2 $HOST $port < /dev/null/dev/tcp — обернуть можно через timeout 1 bash -c "echo >/dev/tcp/$HOST/$port", но это замедляет выполнение и плодит дочерние процессыlnx_shell_susp_commands.yml. Категория Port Scan (#14 по классификации AbuseIPDB) — стандартный триггер для автоматических блокировок. На CTF это обычно не проблема, на реальном пентесте — учитывайтеРаботает если: GNU grep установлен (Kali, Ubuntu — по умолчанию), файл в текстовом формате. Не работает если: используется BSD grep без -P (macOS — заменить на grep -oE), файл бинарный (использовать strings перед grep).
Основная задача в CTF — не запустить сканер, а быстро вытащить из его вывода нужное. Русскоязычные гайды обычно заканчиваются на grep "open" — это покрывает процентов десять реальных задач парсинга. Разберём обработку вывода nmap и curl — двух инструментов, которые генерируют основной поток текстовых данных на соревнованиях.
Ключевой флаг — -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.
На 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-запрос).
Работает если: есть 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 обработает каждый.
На 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 — и помогает понять, за что хвататься первым.
Однострочники — рабочий инструмент 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.
Автоматизация 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 — клей между утилитами, не полноценный язык программирования. И знание его границ экономит часы.
Многопоточность с контролем. Фоновые процессы через & — грубый механизм. Нет пула потоков, нет очереди задач. Для перебора 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 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...