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.
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.
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.
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.
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.
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.
echo "one two three" | wc -wSaída:
3O 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 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.
echo first > notes.txt
echo second >> notes.txt
cat notes.txtSaída:
first
secondO shell abre o arquivo e o esvazia antes de o comando rodar. Por isso, este comando apaga os dados que deveria ordenar:
sort d.txt > d.txtDepois 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.
ls missing-file 2>/dev/null
echo "status=$?"Saída:
status=2O texto do erro foi para o descarte, então o terminal mostra apenas o código de status.
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.
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:
name="a b"
printf "[%s]\n" $name
printf "[%s]\n" "$name"Saída:
[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.
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.
for n in 1 2 3; do
if [ "$n" -eq 2 ]; then
echo "stop at $n"
break
fi
echo "ok $n"
doneSaída:
ok 1
stop at 2O break sai do laço cedo, então o terceiro item nunca roda.
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.
grep -q Failed sample.log
echo "match=$?"
grep -q nomatch sample.log
echo "nomatch=$?"Saída:
match=0
nomatch=1A 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.
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 -c 'false | true; echo "status=$?"'Saída:
status=0O false falhou, mas o true veio por último e teve sucesso, então o pipeline informou sucesso. Com pipefail:
bash -c 'set -o pipefail; false | true; echo "status=$?"'Saída:
status=1Agora o set -e com um comando que falha:
bash -c 'set -e; echo one; false; echo two'Saída:
oneO script parou antes de two. A própria chamada bash -c terminou com status 1.
O set -u pega um argumento ausente:
bash -c 'set -u; echo "$1"' _Saída:
_: line 1: $1: unbound variableA 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 -c 'set -euo pipefail; grep "Nope" sample.log | wc -l; echo "after"'Saída:
0O 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.
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 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.
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
EOFConfira o número de linhas com wc -l sample.log. Ele deve informar 4.
grep 'Failed password' sample.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -nrSaída:
2 10.0.0.5
1 10.0.0.7Leia o pipeline uma etapa de cada vez. Rode os dois primeiros comandos sozinhos para ver o que cada etapa acrescenta.
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.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:sed -n '1p' sample.log | awk '{print NF, $(NF-3)}'Saída:
14 10.0.0.5A linha tem 14 campos, e o campo 11 é o endereço.
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.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ê:
grep 'Failed password' sample.log | awk '{c[$(NF-3)]++} END {for (ip in c) print c[ip], ip}'Saída:
2 10.0.0.5
1 10.0.0.7O 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.
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.
Crie hashall.sh na mesma pasta, usando o mesmo padrão de heredoc com aspas.
cat > hashall.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do [ -f "$f" ] && sha256sum "$f"; done
EOFLeia o script linha por linha:
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:
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:
288797e3d2baa72b086c5fca9821282d06726cb44272647d41128ababf255c94 *./hashall.sh
2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893 *./sample.logO 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:
sha256sum sample.logEle imprime, para sample.log, o mesmo hash que o script imprimiu.
Acrescente uma linha ao log e calcule o hash de novo.
echo "x" >> sample.log
sha256sum sample.logNo 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.
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:
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do
if [ -f "$f" ]; then sha256sum "$f"; fi
doneEm 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.
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.
Linux e ferramentas de desenvolvimento
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.
Linux e ferramentas de desenvolvimento
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.
Linux e ferramentas de desenvolvimento
GitHub para iniciantes: a diferença entre Git e GitHub, os termos básicos, um exercício local com Git e como checar um projeto antes de rodá-lo.