Pular para o conteúdo
Novo

DeepSeek V4.1 Flash grátis por 7 dias: 10M tokens por dia após qualquer recarga. Até 30/11.

Como funciona

Bash básico para trabalho de segurança

Aprenda pipes, redirecionamento, variáveis, laços, códigos de saída e set -euo pipefail, e use isso para contar logins falhos e gerar hashes.

Iniciante14 min de leituraPor NotHoly

O Bash é o shell por trás da maioria dos terminais Linux. Ele também roda no macOS, dentro de muitos containers e no Git Bash do Windows, que foi o ambiente usado neste artigo. Para quem estuda segurança, é a forma mais rápida de fazer uma pergunta a um arquivo de log, verificar se uma pasta mudou ou repetir a mesma checagem em vários arquivos. Você não precisa saber programar para começar. Basta um terminal, um editor de texto e cerca de uma hora.

Este artigo começa pelo que o shell faz e depois cobre os hábitos que tornam o trabalho no shell confiável: pipes, redirecionamento, variáveis, laços e códigos de saída. Em seguida, você vai deixar um script mais rigoroso com set -euo pipefail. Depois, faz dois exercícios pequenos. No primeiro, conta tentativas de login SSH falhas por endereço IP em um log de exemplo. No segundo, escreve um script curto que registra um hash SHA-256 de cada arquivo de uma pasta.

Tudo aqui roda em arquivos que você mesmo cria. Nada deste artigo toca em outro sistema.

O que é o Bash

Bash é a sigla de Bourne Again SHell. Um shell lê as linhas que você digita, executa outros programas e mostra o que eles produzem. O Bash é um entre vários shells, ao lado de sh, zsh e fish. Ele é o padrão em muitas distribuições Linux, e a maioria dos scripts que você encontra na internet pressupõe seu uso.

Dois termos aparecem em todas as seções. Um comando é o nome de um programa seguido de seus argumentos, como grep -c Failed sample.log. Um script é um arquivo de texto com uma lista de comandos, que o Bash lê de cima para baixo. O script permite repetir um trabalho sem digitar tudo de novo, e permite que outra pessoa leia exatamente o que você fez.

A referência para todos os comportamentos deste artigo é o GNU Bash Reference Manual. Quando um resultado parecer estranho, consulte o manual antes de confiar num palpite.

Por que isso importa no trabalho de segurança

A maior parte dos dados de segurança é texto. Logs de autenticação, logs de acesso de servidores web, logs de consultas DNS, relatórios de scanners e listas de hashes chegam como linhas. Boa parte do trabalho diário é fazer perguntas a essas linhas. Quantas vezes este endereço falhou ao entrar? Quais arquivos mudaram desde a semana passada? Qual conta apareceu pela primeira vez hoje?

Uma planilha ou um programa Python também respondem a essas perguntas. O Bash costuma responder mais rápido, em uma linha, e cada etapa fica visível. Para quem está começando, essa visibilidade vale mais do que a velocidade. Quando um resultado parece errado, você executa a primeira parte do pipeline sozinha e vê o que ela produziu.

Comandos de shell também aparecem em guias de instalação, pontos de entrada de containers e rotinas de automação. Ler esses comandos faz parte do trabalho, mesmo quando você preferiria escrever em Python.

Como funciona, passo a passo

Pipes enviam a saída de um programa para o próximo

Um pipe, escrito como |, liga a saída padrão do comando da esquerda à entrada padrão do comando da direita. A maioria das ferramentas de linha de comando lê linhas, faz uma tarefa e imprime linhas. O shell faz a ligação entre elas.

bash
echo "one two three" | wc -w

Saída:

text
3

O wc -w conta palavras. Ele não precisa de nome de arquivo, porque o pipe entrega a ele o texto produzido pelo echo. Programas pequenos ligados por pipes são o hábito central do trabalho no shell.

O redirecionamento decide para onde vai a saída

O redirecionamento muda de onde um comando lê e para onde ele escreve. > grava a saída padrão em um arquivo e substitui o conteúdo anterior. >> acrescenta ao final. 2> envia a saída de erro, o canal em que os programas imprimem suas reclamações, para um arquivo. 2>&1 envia a saída de erro para o mesmo lugar da saída padrão.

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

Saída:

text
first
second

O shell abre o arquivo e o esvazia antes de o comando rodar. Por isso, este comando apaga os dados que deveria ordenar:

bash
sort d.txt > d.txt

Depois dessa linha, d.txt fica vazio. Em um arquivo de teste, o resultado foi zero bytes. Grave em um arquivo novo, confira o resultado e só então substitua o antigo.

A saída de erro importa na segurança porque, muitas vezes, o erro tem a resposta. Um arquivo ausente ou uma mensagem de permissão negada são evidências. Mandar os erros para 2>/dev/null deixa a saída mais silenciosa, mas um arquivo ausente passa a parecer igual a um arquivo sem resultados. Enquanto aprende, mantenha os erros visíveis.

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

Saída:

text
status=2

O texto do erro foi para o descarte, então o terminal mostra apenas o código de status.

Variáveis e aspas

Uma variável guarda um valor. Você atribui sem espaços ao redor do sinal de igual e lê colocando um cifrão na frente do nome.

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

Coloque variáveis entre aspas duplas. Sem aspas, o shell separa o valor nos espaços e pode expandir os pedaços como curingas. A diferença aparece assim que o valor tem um espaço:

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

Saída:

text
[a]
[b]
[a b]

O primeiro printf recebeu duas palavras, porque o shell dividiu o valor. O segundo recebeu uma. O mesmo acontece com um arquivo chamado meu relatorio.txt quando você digita cat $file sem aspas.

Os scripts recebem argumentos em variáveis numeradas. $1 é o primeiro argumento, $2 o segundo. A expressão "${1:-.}" significa "use o primeiro argumento se ele estiver definido e não for vazio, caso contrário use o diretório atual". O script de hash usa essa expressão.

Laços repetem um comando para cada item

Um laço for executa o corpo uma vez para cada palavra de uma lista. Um teste entre colchetes, como [ "$n" -eq 2 ], retorna 0 quando a condição é verdadeira e 1 quando é falsa. Um if usa esse status para escolher um caminho.

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

Saída:

text
ok 1
stop at 2

O break sai do laço cedo, então o terceiro item nunca roda.

Códigos de saída mostram se um comando deu certo

Todo comando termina com um status de saída, um número de 0 a 255. Zero significa sucesso. Qualquer outro valor significa falha, e cada programa define os próprios números. O grep do GNU retorna 0 quando seleciona pelo menos uma linha, 1 quando nada casa e 2 quando ocorre um erro. Leia o status com $? na linha seguinte.

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

Saída:

text
match=0
nomatch=1

A opção -q suprime a saída e só define o status, o que permite usar o grep dentro de um if sem imprimir nada.

Um pipeline tem um único status, e por padrão ele é o status do último comando. A próxima seção muda isso.

Modo estrito: set -euo pipefail

O Bash continua executando depois de erros, a menos que você diga o contrário. Três opções fazem um script falhar cedo. Elas estão documentadas no comando embutido set do manual.

  • set -e interrompe o script quando um comando falha. Comandos dentro de um teste if, e comandos de uma lista && ou || que não sejam o último, não interrompem o script, porque o Bash espera que eles falhem às vezes.
  • set -u trata uma variável não definida como erro, então um erro de digitação no nome da variável para a execução em vez de virar uma string vazia sem aviso.
  • set -o pipefail faz um pipeline falhar se qualquer comando dele falhar, e não apenas o último.

Primeiro, o comportamento padrão:

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

Saída:

text
status=0

O false falhou, mas o true veio por último e teve sucesso, então o pipeline informou sucesso. Com pipefail:

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

Saída:

text
status=1

Agora o set -e com um comando que falha:

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

Saída:

text
one

O script parou antes de two. A própria chamada bash -c terminou com status 1.

O set -u pega um argumento ausente:

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

Saída:

text
_: line 1: $1: unbound variable

A forma combinada, set -euo pipefail, é o que a maioria das pessoas chama de modo estrito. Ela tem um custo. Uma busca que não encontra nada é um resultado normal, mas o grep informa isso com status 1, então, no modo estrito, o script para. Teste o caso antes de depender dele:

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

Saída:

text
0

O script imprimiu a contagem e depois parou sem imprimir after. Se "nenhuma ocorrência" for um resultado válido para o seu script, acrescente || true depois do pipeline, ou verifique o status por conta própria.

Mão na massa: contar logins falhos por IP

Você vai criar um arquivo de log pequeno e contar quantas vezes cada endereço de origem aparece em linhas de senha falha. O log é sintético. Os endereços vêm da faixa privada 10.0.0.0/8, e as linhas seguem o formato das mensagens do OpenSSH em um host de laboratório. Logs reais de autenticação contêm endereços de pessoas reais, então trate-os como dados privados e leia apenas logs que você tem permissão para ler.

Crie o log de exemplo

Crie uma pasta para este laboratório, entre nela e grave o arquivo. O 'EOF' entre aspas impede o Bash de expandir qualquer coisa dentro do texto.

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

Confira o número de linhas com wc -l sample.log. Ele deve informar 4.

Conte as falhas em um único pipeline

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

Saída:

text
      2 10.0.0.5
      1 10.0.0.7

Leia o pipeline uma etapa de cada vez. Rode os dois primeiros comandos sozinhos para ver o que cada etapa acrescenta.

  1. grep 'Failed password' sample.log mantém as três linhas de falha e descarta a linha Accepted. Confirme a contagem com grep -c 'Failed password' sample.log, que imprime 3.
  2. awk '{print $(NF-3)}' imprime um campo de cada linha. NF guarda o número de campos da linha atual. Cada linha termina com from ENDEREÇO port PORTA ssh2, então o endereço é o quarto campo contando do fim, que é $(NF-3). Confirme na primeira linha:
bash
sed -n '1p' sample.log | awk '{print NF, $(NF-3)}'

Saída:

text
14 10.0.0.5

A linha tem 14 campos, e o campo 11 é o endereço.

  1. sort coloca endereços iguais lado a lado. uniq -c conta duplicatas adjacentes e imprime a contagem na frente. Sem o sort, o uniq contaria apenas repetições vizinhas, por isso a ordenação vem sempre antes.
  2. sort -nr ordena as contagens numericamente, da maior para a menor.

O número do campo no awk pertence a este formato de log. Se você aplicar o mesmo comando a outro log, confira a posição do campo primeiro com a linha do sed acima. Um número que funcionou uma vez não prova que servirá para o próximo arquivo.

O mesmo resultado pode vir de um único programa awk, porque os arrays do awk fazem a contagem para você:

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

Saída:

text
2 10.0.0.5
1 10.0.0.7

O awk não garante uma ordem para for (ip in c), então ordene o resultado se a ordem importar. O GNU Awk User's Guide explica arrays e a regra END.

Mão na massa: calcular o hash de cada arquivo de uma pasta

Um hash é uma impressão digital de tamanho fixo do conteúdo de um arquivo. Se você registrar hoje o hash SHA-256 de cada arquivo de uma pasta, pode rodar o mesmo comando na semana que vem e ver quais arquivos mudaram. Um hash não prova quem fez a mudança nem que o arquivo é seguro. Ele mostra apenas se os bytes mudaram desde que você os registrou.

Escreva o script

Crie hashall.sh na mesma pasta, usando o mesmo padrão de heredoc com aspas.

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

Leia o script linha por linha:

  • A primeira linha é o shebang. Ela diz ao sistema para executar o arquivo com o Bash, encontrado pelo env.
  • set -euo pipefail liga o modo estrito, como visto antes.
  • "${1:-.}"/* é a pasta que você passa, ou a pasta atual quando você não passa nada. O * é um glob. O shell o substitui pelos nomes da pasta, em ordem. Nomes que começam com ponto ficam de fora por padrão, como a seção de expansão de nomes de arquivo do manual descreve.
  • [ -f "$f" ] && sha256sum "$f" calcula o hash apenas de arquivos regulares. O sha256sum aplicado a uma pasta imprimiria erro, então o teste deixa as pastas de fora.

Torne o script executável e rode-o na pasta atual:

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

O chmod +x adiciona a permissão de execução. O ./ diz ao shell para executar o arquivo desta pasta, porque a pasta atual não está no PATH por padrão.

Saída em uma pasta que tinha apenas hashall.sh e sample.log:

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

O Git Bash imprime um asterisco antes de cada nome, que marca o modo binário no GNU coreutils. Outros sistemas podem mostrar um espaço. O valor do hash continua o mesmo. Se os seus valores forem diferentes, confira se os arquivos são idênticos byte a byte, inclusive as quebras de linha.

Confira um valor à mão:

bash
sha256sum sample.log

Ele imprime, para sample.log, o mesmo hash que o script imprimiu.

Veja um hash mudar

Acrescente uma linha ao log e calcule o hash de novo.

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

No teste, o hash passou a ser 3084d4c5a7659a2f3636b6079636b081b7ab1b6fca6a5e691c3afdd160959687, no lugar de 2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893. O arquivo cresceu dois bytes e a impressão digital mudou junto. Restaure o arquivo original a partir da sua cópia antes de continuar.

Erros comuns

Variáveis e caminhos sem aspas. cat $file quebra em nomes com espaços. Use aspas por padrão e retire-as só quando souber que o valor não pode ter espaços nem curingas.

Sobrescrever um arquivo de entrada com >. sort d.txt > d.txt esvazia o arquivo antes de o sort lê-lo. Grave em um arquivo novo, confira a saída e depois mova-o para o lugar certo.

Confiar em um número de campo sem testar. $(NF-3) funcionou com a amostra. Teste a posição do campo em algumas linhas reais antes de montar um relatório em cima dela.

Modo estrito interrompendo uma busca sem resultados. Como mostrado acima, o grep retornando 1 encerra um pipeline sob pipefail. Decida se "nenhuma ocorrência" é erro para o seu script e trate isso de forma intencional.

Um laço que informa o status errado. O hashall.sh acima calcula o hash de todo arquivo regular. Quando o último nome da pasta é uma subpasta, porém, o script imprime todos os hashes e depois sai com status 1. O status vinha do último teste [ -f ], que falhou. A forma explícita com if faz o mesmo trabalho e sai com 0:

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

Em uma pasta de teste com uma subpasta como último item, essa versão imprimiu o hash de cada arquivo e saiu com status 0. A versão com && também sai com status 1 em uma pasta vazia, então a forma com if é a melhor escolha padrão.

Executar scripts que você não escreveu. Um script pode apagar ou enviar arquivos com a mesma facilidade com que calcula hashes. Leia o arquivo inteiro antes, confira cada comando que ele chama e rode-o em uma cópia da sua pasta.

Como continuar

Leia as seções do GNU Bash Reference Manual sobre pipelines, redirecionamentos, expansão de parâmetros e o comando set. Elas cobrem a maior parte do que você viu aqui. Use a parte sobre códigos de saída como referência quando um script se comportar de forma inesperada.

Depois, leia o manual do GNU grep. Teste grep -i, grep -v e grep -E no seu log de exemplo e observe como cada opção muda o status de saída.

O GNU Awk User's Guide cobre campos, padrões e arrays. O contador de uma linha acima é um pequeno exemplo do que o awk faz. Quando isso parecer natural, escreva um segundo padrão que conte logins aceitos por usuário.

Mantenha os scripts curtos. Um script de dez linhas que você consegue explicar linha por linha é mais útil do que um de cem linhas copiado que você não sabe modificar. Quando um script passa de uma tela, separe-o em funções ou considere uma linguagem com estruturas de dados melhores para a tarefa.

Fontes

Fontes

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

Mais em Linux e ferramentas de desenvolvimento

Linux e ferramentas de desenvolvimento

Permissões no Linux: usuários, grupos, chmod e umask

Donos, grupos, bits rwx, chmod, umask e setuid no Linux, com laboratório que testa o acesso de outro usuário e procura modos arriscados.

Iniciante13 min de leitura

Linux e ferramentas de desenvolvimento

Chaves SSH: como funcionam e como usar com segurança

Como funcionam as chaves SSH de host e de usuário, por que Ed25519 com senha de proteção é o padrão e como proteger o login em uma VM de laboratório.

Iniciante11 min de leitura