Перейти к содержимому
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

Основы Bash для работы в безопасности

Пайпы, перенаправление, переменные, циклы, коды завершения и set -euo pipefail, а затем подсчёт неудачных входов и хеширование папки на этой основе.

Начальный11 мин чтенияАвтор: NotHoly

Bash — это оболочка, которая стоит за большинством терминалов Linux. Ещё он работает на macOS, внутри многих контейнеров и в Git Bash под Windows, который и использовался для этой статьи. Для студента, изучающего безопасность, это самый быстрый способ задать вопрос лог-файлу, проверить папку на изменения или прогнать одну проверку по множеству файлов. Программировать для этого не нужно: хватит терминала, текстового редактора и примерно часа времени.

Сначала статья объясняет, что делает оболочка, потом переходит к привычкам, которые делают работу в ней надёжной: пайпам, перенаправлению, переменным, циклам и кодам завершения. После этого вы ужесточите скрипт с помощью set -euo pipefail и сделаете два небольших упражнения. В первом вы посчитаете неудачные входы по SSH для каждого IP-адреса в тестовом логе. Во втором напишете короткий скрипт, который записывает хеш SHA-256 каждого файла в папке.

Всё здесь выполняется на файлах, которые вы создаёте сами. Ни одна другая система не затрагивается.

Что такое Bash

Bash расшифровывается как Bourne Again SHell. Оболочка читает введённые вами строки, запускает другие программы и печатает то, что они выводят. Bash — это одна из нескольких оболочек, наряду с sh, zsh и fish. Во многих дистрибутивах Linux он стоит по умолчанию, и большинство скриптов, которые вы найдёте в сети, рассчитаны на него.

В каждом разделе встречаются два термина. Команда — это имя программы и её аргументы, например grep -c Failed sample.log. Скрипт — это текстовый файл со списком команд, который Bash читает сверху вниз. Скрипт избавляет от повторного набора и показывает другому человеку, что именно вы сделали.

За справкой по всему описанному здесь поведению обращайтесь к GNU Bash Reference Manual. Если результат вас удивил, загляните в руководство, прежде чем верить догадке.

Зачем это в работе по безопасности

Большая часть данных в безопасности — это текст. Журналы аутентификации, логи доступа веб-сервера, логи DNS-запросов, отчёты сканеров и списки хешей приходят построчно. Значительная часть повседневной работы сводится к вопросам к этим строкам. Сколько раз этот адрес не смог войти? Какие файлы изменились с прошлой недели? Какая учётная запись появилась сегодня впервые?

На каждый из этих вопросов ответит таблица или программа на Python. Bash часто отвечает быстрее, одной строкой, и каждый шаг остаётся на виду. Для новичка такая наглядность важнее скорости. Если результат выглядит неверно, вы запускаете первую часть конвейера отдельно и смотрите, что она выдала.

Команды оболочки встречаются ещё в руководствах по установке, точках входа контейнеров и задачах автоматизации. Читать их приходится, даже если писать вы предпочли бы на Python.

Как это работает, шаг за шагом

Пайп передаёт вывод одной программы на вход следующей

Пайп, он же |, соединяет стандартный вывод команды слева со стандартным вводом команды справа. Большинство инструментов командной строки читают строки, делают одну работу и печатают строки. Соединяет их оболочка.

bash
echo "one two three" | wc -w

Вывод:

text
3

wc -w считает слова. Имя файла ей не нужно, потому что пайп подаёт текст от echo. Соединять маленькие программы пайпами — основная привычка в работе с оболочкой.

Перенаправление определяет, куда идёт вывод

Перенаправление меняет, откуда команда читает и куда пишет. > записывает стандартный вывод в файл и заменяет его содержимое. >> дописывает в конец. 2> отправляет в файл стандартный поток ошибок, канал, куда программы печатают свои жалобы. 2>&1 направляет поток ошибок туда же, куда идёт стандартный вывод.

bash
echo first > notes.txt
echo second >> notes.txt
cat notes.txt

Вывод:

text
first
second

Оболочка открывает и очищает файл ещё до запуска команды. Поэтому следующая команда уничтожает данные, которые должна была отсортировать:

bash
sort d.txt > d.txt

После этой строки d.txt пуст. На тестовом файле получилось ноль байт. Пишите в новый файл, проверяйте результат и только потом заменяйте старый.

Поток ошибок важен в работе по безопасности, потому что в ошибках часто лежит ответ. Сообщение об отсутствующем файле или отказе в доступе тоже свидетельство. Перенаправление ошибок через 2>/dev/null делает вывод тише, но тогда отсутствующий файл выглядит так же, как файл без совпадений. Пока учитесь, оставляйте ошибки видимыми.

bash
ls missing-file 2>/dev/null
echo "status=$?"

Вывод:

text
status=2

Текст ошибки ушёл в файл для ненужного вывода, поэтому в терминале виден только код статуса.

Переменные и кавычки

Переменная хранит значение. Присваивайте его без пробелов вокруг знака равенства. Читайте, поставив перед именем знак доллара.

bash
target="10.0.0.5"
echo "Checking $target"

Заключайте переменные в двойные кавычки. Переменная без кавычек разбивается по пробелам, а куски могут раскрыться как подстановочные шаблоны. Разница видна, как только в значении появляется пробел:

bash
name="a b"
printf "[%s]\n" $name
printf "[%s]\n" "$name"

Вывод:

text
[a]
[b]
[a b]

Первый printf получил два слова, потому что оболочка разбила значение. Второй получил одно. То же самое случится с файлом my report.txt, если набрать cat $file без кавычек.

Скрипты получают аргументы как нумерованные переменные. В $1 лежит первый аргумент, в $2 второй. Выражение "${1:-.}" означает «взять первый аргумент, если он задан и не пуст, иначе взять текущий каталог». Оно используется в скрипте хеширования.

Цикл повторяет команду для каждого элемента

Цикл for выполняет своё тело по одному разу на каждое слово из списка. Проверка в квадратных скобках, например [ "$n" -eq 2 ], возвращает 0, если условие истинно, и 1, если ложно. По этому статусу оператор if выбирает ветку.

bash
for n in 1 2 3; do
  if [ "$n" -eq 2 ]; then
    echo "stop at $n"
    break
  fi
  echo "ok $n"
done

Вывод:

text
ok 1
stop at 2

break досрочно выходит из цикла, поэтому до третьего элемента дело не доходит.

Коды завершения показывают, успешно ли прошла команда

Каждая команда завершается с кодом, числом от 0 до 255. Ноль означает успех. Всё остальное означает неудачу, и значения кодов каждая программа определяет сама. GNU grep возвращает 0, если выбрал хотя бы одну строку, 1, если совпадений нет, и 2 при ошибке. Прочитать статус можно через $? на следующей строке.

bash
grep -q Failed sample.log
echo "match=$?"
grep -q nomatch sample.log
echo "nomatch=$?"

Вывод:

text
match=0
nomatch=1

Опция -q подавляет вывод и только выставляет статус, так что grep можно использовать в if, ничего не печатая.

У конвейера один статус, и по умолчанию это статус последней команды. В следующем разделе это изменится.

Строгий режим: set -euo pipefail

Bash продолжает работу после ошибок, пока ему не скажут иначе. Три опции заставляют скрипт падать сразу. Они описаны в руководстве в разделе о встроенной команде set.

  • set -e останавливает скрипт при сбое команды. Команды в проверке if и команды в списке && или ||, кроме последней, его не останавливают, потому что Bash допускает, что они иногда завершаются неудачей.
  • set -u считает незаданную переменную ошибкой, и опечатка в имени переменной останавливает запуск вместо того, чтобы тихо превратиться в пустую строку.
  • set -o pipefail заставляет конвейер завершаться неудачей, если сбоит любая его команда, а не только последняя.

Сначала поведение по умолчанию:

bash
bash -c 'false | true; echo "status=$?"'

Вывод:

text
status=0

false завершилась неудачей, но последней шла успешная true, поэтому конвейер сообщил об успехе. С pipefail:

bash
bash -c 'set -o pipefail; false | true; echo "status=$?"'

Вывод:

text
status=1

Теперь set -e со сбойной командой:

bash
bash -c 'set -e; echo one; false; echo two'

Вывод:

text
one

Скрипт остановился до two. Сам вызов bash -c завершился со статусом 1.

set -u ловит отсутствующий аргумент:

bash
bash -c 'set -u; echo "$1"' _

Вывод:

text
_: line 1: $1: unbound variable

Объединённая форма set -euo pipefail и есть то, что обычно называют строгим режимом. У неё есть обратная сторона. Поиск без результатов вполне нормален, но grep сообщает о нём статусом 1, и в строгом режиме скрипт останавливается. Проверьте этот случай, прежде чем на него полагаться:

bash
bash -c 'set -euo pipefail; grep "Nope" sample.log | wc -l; echo "after"'

Вывод:

text
0

Скрипт напечатал счётчик и остановился, не напечатав after. Если «нет совпадений» для вашего скрипта допустимый результат, добавьте || true после конвейера или проверяйте статус сами.

Практика: считаем неудачные входы по IP

Вы соберёте небольшой лог-файл и посчитаете, как часто каждый адрес источника встречается в строках с неудачным паролем. Лог синтетический. Адреса взяты из частного диапазона 10.0.0.0/8, а строки повторяют формат сообщений OpenSSH на лабораторном хосте. В настоящих журналах аутентификации лежат адреса реальных людей, поэтому обращайтесь с ними как с личными данными и читайте только те логи, которые вам разрешено читать.

Создаём тестовый лог

Создайте папку для этой лабораторной, перейдите в неё и запишите файл. Кавычки в 'EOF' не дают Bash раскрывать что-либо внутри текста.

bash
cat > sample.log <<'EOF'
Jan 10 10:00:01 lab sshd[1]: Failed password for root from 10.0.0.5 port 5000 ssh2
Jan 10 10:00:03 lab sshd[2]: Failed password for admin from 10.0.0.5 port 5001 ssh2
Jan 10 10:01:00 lab sshd[3]: Accepted publickey for alice from 10.0.0.9 port 5002 ssh2
Jan 10 10:02:00 lab sshd[4]: Failed password for bob from 10.0.0.7 port 5003 ssh2
EOF

Проверьте количество строк с помощью wc -l sample.log. Команда сообщит 4.

Считаем неудачи одним конвейером

bash
grep 'Failed password' sample.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -nr

Вывод:

text
      2 10.0.0.5
      1 10.0.0.7

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

  1. grep 'Failed password' sample.log оставляет три строки с неудачей и отбрасывает строку Accepted. Подтвердите количество командой grep -c 'Failed password' sample.log, она напечатает 3.
  2. awk '{print $(NF-3)}' печатает одно поле из каждой строки. В NF лежит число полей текущей строки. Каждая строка заканчивается на from ADDRESS port PORT ssh2, так что адрес стоит четвёртым полем с конца, то есть $(NF-3). Проверьте на первой строке:
bash
sed -n '1p' sample.log | awk '{print NF, $(NF-3)}'

Вывод:

text
14 10.0.0.5

В строке 14 полей, и адрес стоит в поле 11.

  1. sort ставит одинаковые адреса рядом. uniq -c считает идущие подряд дубликаты и печатает счётчик перед ними. Без sort команда uniq посчитала бы только соседние повторы, поэтому сортировка всегда идёт первой.
  2. sort -nr сортирует счётчики как числа, от большего к меньшему.

Номер поля в awk относится к этому формату лога. Если направить ту же команду на другой лог, сначала проверьте положение поля строкой с sed выше. То, что номер сработал один раз, ещё не доказывает, что он подойдёт для следующего файла.

Тот же результат даст одна программа на awk, потому что массивы awk считают сами:

bash
grep 'Failed password' sample.log | awk '{c[$(NF-3)]++} END {for (ip in c) print c[ip], ip}'

Вывод:

text
2 10.0.0.5
1 10.0.0.7

awk не обещает порядка для for (ip in c), поэтому сортируйте результат, если порядок важен. Массивы и правило END описаны в GNU Awk User's Guide.

Практика: хешируем все файлы в папке

Хеш — это отпечаток содержимого файла фиксированной длины. Если сегодня записать хеш SHA-256 каждого файла в папке, через неделю можно запустить ту же команду и увидеть, какие файлы изменились. Хеш не доказывает, кто внёс изменение и безопасен ли файл. Он показывает только, менялись ли байты с момента записи.

Пишем скрипт

Создайте hashall.sh в той же папке по тому же шаблону с heredoc в кавычках.

bash
cat > hashall.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do [ -f "$f" ] && sha256sum "$f"; done
EOF

Разберём скрипт построчно:

  • В первой строке стоит шебанг. Он говорит системе запускать файл через Bash, найденный с помощью env.
  • set -euo pipefail включает строгий режим, как описано выше.
  • "${1:-.}"/* — это папка, которую вы передали, или текущая папка, если ничего не передано. * — это glob-шаблон. Оболочка заменяет его отсортированными именами из этой папки. Имена, начинающиеся с точки, по умолчанию пропускаются, как сказано в разделе руководства о раскрытии имён файлов.
  • [ -f "$f" ] && sha256sum "$f" хеширует только обычные файлы. sha256sum на папке напечатала бы ошибку, поэтому проверка отсеивает папки.

Сделайте скрипт исполняемым и запустите его на текущей папке:

bash
chmod +x hashall.sh
./hashall.sh .

chmod +x добавляет право на выполнение. ./ указывает оболочке запускать файл из этой папки, потому что текущей папки по умолчанию нет в PATH.

Вывод в папке, где лежали только hashall.sh и sample.log:

text
288797e3d2baa72b086c5fca9821282d06726cb44272647d41128ababf255c94 *./hashall.sh
2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893 *./sample.log

Git Bash печатает звёздочку перед каждым именем, в GNU coreutils она обозначает двоичный режим. Другие системы могут показать вместо неё пробел. Сам хеш остаётся тем же. Если ваши значения отличаются, проверьте, что файлы совпадают с текстом байт в байт, включая окончания строк.

Подтвердите одно значение вручную:

bash
sha256sum sample.log

Команда напечатает для sample.log тот же хеш, что и скрипт.

Смотрим, как меняется хеш

Допишите в лог одну строку и снова посчитайте хеш.

bash
echo "x" >> sample.log
sha256sum sample.log

В тестовом запуске хеш стал 3084d4c5a7659a2f3636b6079636b081b7ab1b6fca6a5e691c3afdd160959687, а был 2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893. Файл вырос на два байта, и отпечаток изменился вместе с ним. Прежде чем продолжать, восстановите исходный файл из своей копии.

Частые ошибки

Переменные и пути без кавычек. cat $file ломается на именах с пробелами. Ставьте кавычки по умолчанию и убирайте их, только если точно знаете, что в значении не может быть пробелов и подстановочных шаблонов.

Перезапись входного файла через >. sort d.txt > d.txt очищает файл до того, как sort его прочитает. Пишите в новый файл, проверяйте вывод и только потом перемещайте его на место.

Доверие номеру поля. $(NF-3) сработало на примере. Проверьте положение поля на нескольких настоящих строках, прежде чем строить на нём отчёт.

Падение в строгом режиме на допустимом «нет совпадений». Как показано выше, статус 1 от grep завершает конвейер при pipefail. Решите, считается ли отсутствие совпадений ошибкой для вашего скрипта, и обработайте это осознанно.

Цикл с неверным статусом. hashall.sh выше хеширует каждый обычный файл. Но если последней в папке идёт подпапка, скрипт напечатает все хеши и завершится со статусом 1. Этот статус остался от последней проверки [ -f ], которая не прошла. Явная форма с if делает ту же работу и завершается с 0:

bash
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do
  if [ -f "$f" ]; then sha256sum "$f"; fi
done

В тестовой папке, где последней шла подпапка, эта версия напечатала хеш каждого файла и завершилась со статусом 0. Версия с && на пустой папке тоже завершается со статусом 1, так что форму с if лучше брать по умолчанию.

Запуск чужих скриптов. Скрипт может удалить или отправить файлы так же легко, как и посчитать их хеши. Сначала прочитайте весь файл, проверьте каждую команду, которую он вызывает, и запустите его на копии папки.

Что дальше

Прочитайте разделы GNU Bash Reference Manual о конвейерах, перенаправлениях, раскрытии параметров оболочки и встроенной команде set. Они охватывают большую часть увиденного здесь. Материал о кодах завершения держите как справочник на случай, если скрипт поведёт себя неожиданно.

Следующим прочитайте руководство GNU grep. Попробуйте grep -i, grep -v и grep -E на своём тестовом логе и отметьте, как каждая опция меняет код завершения.

Поля, шаблоны и массивы описаны в GNU Awk User's Guide. Однострочный счётчик выше показывает малую часть возможностей awk. Когда он станет привычным, напишите второй шаблон, который считает успешные входы по пользователям.

Держите скрипты короткими. Скрипт на десять строк, который вы можете объяснить построчно, полезнее скопированного скрипта на сто строк, который вы не умеете менять. Когда скрипт перерастает экран, разбейте его на функции или подумайте о языке с более подходящими для задачи структурами данных.

Источники

Источники

  1. GNU Bash Reference Manual
  2. GNU grep manual
  3. GNU Awk User's Guide

Ещё по теме: Linux и инструменты разработчика

Linux и инструменты разработчика

Права в Linux: пользователи, группы, chmod и umask

Владельцы и группы в Linux, биты rwx, chmod, umask и setuid, а также лабораторная, где доступ проверяется от имени второго пользователя и ищутся опасные режимы файлов.

Начальный11 мин чтения

Linux и инструменты разработчика

Ключи SSH: как они работают и как пользоваться ими безопасно

Как работают ключи хоста и пользователя в SSH, почему по умолчанию стоит брать Ed25519 с парольной фразой и как настроить и защитить вход по ключу на своей лабораторной VM.

Начальный9 мин чтения