Vai al contenuto
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

Basi di Bash per il lavoro di sicurezza

Impara pipe, redirezioni, variabili, loop, codici di uscita e set -euo pipefail, poi usali per contare gli accessi falliti e calcolare l'hash di una cartella.

Base13 min di letturaDi NotHoly

Bash è la shell dietro la maggior parte dei terminali Linux. Gira anche su macOS, in molti container e in Git Bash su Windows, che è quello usato per questo articolo. Per chi studia sicurezza è il modo più veloce di fare una domanda a un file di log, controllare se una cartella è cambiata o ripetere lo stesso controllo su molti file. Non serve saper programmare. Ti servono un terminale, un editor di testo e circa un'ora.

L'articolo parte da cosa fa la shell, poi passa alle abitudini che rendono affidabile il lavoro in shell: pipe, redirezioni, variabili, loop e codici di uscita. Dopo renderai più rigoroso uno script con set -euo pipefail. Poi farai due piccoli esercizi. Nel primo conterai gli accessi SSH falliti per indirizzo IP in un log di esempio. Nel secondo scriverai un breve script che registra un hash SHA-256 per ogni file di una cartella.

Tutto gira su file che crei tu. Nulla in questo articolo tocca un altro sistema.

Cos'è Bash

Bash sta per Bourne Again SHell. Una shell legge le righe che scrivi, avvia altri programmi e stampa ciò che producono. Bash è una delle varie shell, insieme a sh, zsh e fish. È quella predefinita su molte distribuzioni Linux, e la maggior parte degli script che trovi online la dà per scontata.

Due termini tornano in ogni sezione. Un comando è il nome di un programma più i suoi argomenti, come grep -c Failed sample.log. Uno script è un file di testo che elenca comandi, e Bash lo legge dall'alto in basso. Con uno script ripeti il lavoro senza riscriverlo, e qualcun altro può leggere esattamente cosa hai fatto.

Il riferimento per ogni comportamento descritto qui è il GNU Bash Reference Manual. Quando un risultato ti sorprende, controlla il manuale prima di fidarti di un'ipotesi.

Perché serve nel lavoro di sicurezza

Quasi tutti i dati di sicurezza sono testo. Log di autenticazione, access log dei web server, log delle query DNS, report degli scanner e liste di hash arrivano tutti come righe. Buona parte del lavoro quotidiano consiste nel fare domande a quelle righe. Quante volte questo indirizzo ha fallito l'accesso? Quali file sono cambiati dalla settimana scorsa? Quale account è comparso per la prima volta oggi?

Un foglio di calcolo o un programma Python possono rispondere a ognuna di queste domande. Bash spesso risponde prima, in una riga, e ogni passaggio resta visibile. Per chi inizia, questa visibilità conta più della velocità. Quando un risultato sembra sbagliato, esegui da sola la prima parte della pipeline e guardi cosa ha prodotto.

I comandi di shell compaiono anche nelle guide di installazione, negli entry point dei container e nei job di automazione. Leggerli fa parte del lavoro, anche quando preferiresti scrivere Python.

Come funziona, passo dopo passo

Le pipe passano l'output di un programma al successivo

Una pipe, scritta |, collega lo standard output del comando a sinistra allo standard input del comando a destra. La maggior parte degli strumenti a riga di comando legge righe, fa una cosa sola e stampa righe. La shell li unisce.

bash
echo "one two three" | wc -w

Output:

text
3

wc -w conta le parole. Non ha bisogno di un nome di file perché la pipe gli passa il testo di echo. Piccoli programmi uniti da pipe sono l'abitudine centrale del lavoro in shell.

La redirezione decide dove va l'output

La redirezione cambia da dove un comando legge o dove scrive. > scrive lo standard output in un file e ne sostituisce il contenuto. >> aggiunge in fondo. 2> manda su un file lo standard error, il canale su cui i programmi stampano i loro lamenti. 2>&1 manda lo standard error nello stesso posto dello standard output.

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

Output:

text
first
second

La shell apre e svuota il file prima che il comando parta. Per questo il comando seguente distrugge i dati che doveva ordinare:

bash
sort d.txt > d.txt

Dopo quella riga, d.txt è vuoto. Su un file di prova, il risultato è stato zero byte. Scrivi su un file nuovo, controlla il risultato, poi sostituisci quello vecchio.

Lo standard error conta nel lavoro di sicurezza perché gli errori spesso contengono la risposta. Un file mancante o un messaggio di permesso negato sono prove. Redirigere gli errori con 2>/dev/null rende l'output più pulito, ma a quel punto un file mancante sembra uguale a un file senza corrispondenze. Mentre impari, lascia gli errori visibili.

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

Output:

text
status=2

Il testo dell'errore è finito nel file di scarto, quindi il terminale mostra solo il codice di stato.

Variabili e virgolette

Una variabile conserva un valore. Assegnalo senza spazi intorno al segno di uguale. Leggilo mettendo il simbolo del dollaro davanti al nome.

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

Racchiudi le variabili tra virgolette doppie. Una variabile senza virgolette viene spezzata agli spazi, e i pezzi possono essere espansi come caratteri jolly. La differenza si vede appena un valore contiene uno spazio:

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

Output:

text
[a]
[b]
[a b]

Il primo printf ha ricevuto due parole, perché la shell ha spezzato il valore. Il secondo ne ha ricevuta una. La stessa cosa succede con un file chiamato my report.txt quando scrivi cat $file senza virgolette.

Gli script ricevono gli argomenti come variabili numerate. $1 è il primo argomento, $2 il secondo. L'espressione "${1:-.}" significa "usa il primo argomento se è impostato e non è vuoto, altrimenti usa la directory corrente". Lo script di hashing la usa.

I loop ripetono un comando per ogni elemento

Un loop for esegue il suo corpo una volta per ogni parola di una lista. Un test tra parentesi quadre, come [ "$n" -eq 2 ], restituisce 0 quando la condizione è vera e 1 quando è falsa. Un'istruzione if usa quello stato per scegliere un ramo.

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

Output:

text
ok 1
stop at 2

break esce dal loop in anticipo, quindi il terzo elemento non viene mai eseguito.

I codici di uscita dicono se un comando è riuscito

Ogni comando termina con uno stato di uscita, un numero da 0 a 255. Zero significa successo. Qualsiasi altro valore significa errore, e ogni programma definisce i propri numeri. GNU grep restituisce 0 quando seleziona almeno una riga, 1 quando non trova nulla e 2 in caso di errore. Leggi lo stato con $? nella riga successiva.

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

Output:

text
match=0
nomatch=1

L'opzione -q sopprime l'output e imposta solo lo stato, così puoi usare grep in un if senza stampare nulla.

Una pipeline ha un solo stato, e per impostazione predefinita è quello del suo ultimo comando. La prossima sezione cambia questo comportamento.

Modalità rigorosa: set -euo pipefail

Bash continua dopo gli errori, a meno che tu non gli dica di fermarsi. Tre opzioni fanno fallire subito uno script. Sono documentate nel manuale alla voce del builtin set.

  • set -e ferma lo script quando un comando fallisce. I comandi in un test if, e quelli in una lista && o || diversi dall'ultimo, non lo fermano, perché Bash si aspetta che a volte falliscano.
  • set -u tratta come errore una variabile non impostata, quindi un refuso nel nome di una variabile ferma l'esecuzione invece di trasformarsi in silenzio in una stringa vuota.
  • set -o pipefail fa fallire una pipeline se fallisce uno qualsiasi dei suoi comandi, e non solo l'ultimo.

Prima il comportamento predefinito:

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

Output:

text
status=0

false è fallito, ma true è arrivato per ultimo ed è riuscito, quindi la pipeline ha riportato successo. Con pipefail:

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

Output:

text
status=1

Ora set -e con un comando che fallisce:

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

Output:

text
one

Lo script si è fermato prima di two. La chiamata bash -c è uscita con stato 1.

set -u intercetta un argomento mancante:

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

Output:

text
_: line 1: $1: unbound variable

La forma combinata, set -euo pipefail, è ciò che quasi tutti intendono per modalità rigorosa. Ha un prezzo. Una ricerca che non trova nulla è un risultato normale, ma grep lo segnala con stato 1, quindi in modalità rigorosa lo script si ferma. Prova il caso prima di farci affidamento:

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

Output:

text
0

Lo script ha stampato il conteggio e si è fermato senza stampare after. Se per il tuo script "nessuna corrispondenza" è un risultato valido, aggiungi || true dopo la pipeline, oppure controlla tu lo stato.

Pratica: conta gli accessi falliti per IP

Costruirai un piccolo file di log, poi conterai quante volte ogni indirizzo di origine compare nelle righe con password errata. Il log è sintetico. Gli indirizzi vengono dall'intervallo privato 10.0.0.0/8 e le righe seguono il formato dei messaggi OpenSSH su un host di laboratorio. I log di autenticazione reali contengono indirizzi di persone reali, quindi trattali come dati privati e leggi solo i log che hai il permesso di leggere.

Crea il log di esempio

Crea una cartella per questo laboratorio, entraci e scrivi il file. Le virgolette in 'EOF' impediscono a Bash di espandere il testo all'interno.

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

Controlla il numero di righe con wc -l sample.log. Riporta 4.

Conta i fallimenti con una sola pipeline

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

Output:

text
      2 10.0.0.5
      1 10.0.0.7

Leggi la pipeline un passaggio alla volta. Esegui separatamente i primi due comandi per vedere cosa aggiunge ognuno.

  1. grep 'Failed password' sample.log tiene le tre righe di accesso fallito e scarta la riga Accepted. Conferma il conteggio con grep -c 'Failed password' sample.log, che stampa 3.
  2. awk '{print $(NF-3)}' stampa un campo di ogni riga. NF contiene il numero di campi della riga corrente. Ogni riga finisce con from ADDRESS port PORT ssh2, quindi l'indirizzo è il quarto campo dalla fine, cioè $(NF-3). Verificalo sulla prima riga:
bash
sed -n '1p' sample.log | awk '{print NF, $(NF-3)}'

Output:

text
14 10.0.0.5

La riga ha 14 campi, e il campo 11 è l'indirizzo.

  1. sort mette vicini gli indirizzi identici. uniq -c conta i duplicati adiacenti e stampa il conteggio davanti. Senza sort, uniq conterebbe solo le ripetizioni vicine, quindi l'ordinamento viene sempre prima.
  2. sort -nr ordina i conteggi numericamente, dal più grande.

Il numero del campo di awk vale per questo formato di log. Se usi lo stesso comando su un log diverso, controlla prima la posizione del campo con la riga sed qui sopra. Un numero che ha funzionato una volta non prova che vada bene per il file successivo.

Lo stesso risultato si ottiene con un solo programma awk, perché gli array di awk contano per te:

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

Output:

text
2 10.0.0.5
1 10.0.0.7

awk non garantisce un ordine per for (ip in c), quindi ordina il risultato se l'ordine conta. Il GNU Awk User's Guide spiega gli array e la regola END.

Pratica: calcola l'hash di ogni file in una cartella

Un hash è un'impronta a lunghezza fissa del contenuto di un file. Se oggi registri l'hash SHA-256 di ogni file di una cartella, la settimana prossima puoi eseguire lo stesso comando e vedere quali file sono cambiati. Un hash non prova chi ha fatto una modifica né se un file è sicuro. Mostra solo se i byte sono cambiati da quando li hai registrati.

Scrivi lo script

Crea hashall.sh nella stessa cartella, con lo stesso schema di heredoc tra virgolette.

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

Leggi lo script riga per riga:

  • La prima riga è lo shebang. Dice al sistema di eseguire il file con Bash, trovato tramite env.
  • set -euo pipefail attiva la modalità rigorosa, come visto prima.
  • "${1:-.}"/* è la cartella che passi, o la cartella corrente se non passi nulla. L'* è un glob. La shell lo sostituisce con i nomi presenti in quella cartella, in ordine. I nomi che iniziano con un punto vengono saltati per impostazione predefinita, come descrive la sezione del manuale sull'espansione dei nomi di file.
  • [ -f "$f" ] && sha256sum "$f" calcola l'hash solo dei file regolari. sha256sum su una cartella stamperebbe un errore, quindi il test esclude le cartelle.

Rendi eseguibile lo script e lancialo sulla cartella corrente:

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

chmod +x aggiunge il permesso di esecuzione. ./ dice alla shell di eseguire il file di questa cartella, perché per impostazione predefinita la cartella corrente non è in PATH.

Output in una cartella che conteneva solo hashall.sh e sample.log:

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

Git Bash stampa un asterisco prima di ogni nome, che in GNU coreutils indica la modalità binaria. Altri sistemi possono mostrare uno spazio. L'hash resta lo stesso. Se i tuoi valori sono diversi, controlla che i file coincidano con il testo byte per byte, fine riga compresi.

Conferma un valore a mano:

bash
sha256sum sample.log

Stampa per sample.log lo stesso hash stampato dallo script.

Guarda un hash che cambia

Aggiungi una riga al log, poi ricalcola l'hash.

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

Nell'esecuzione di prova l'hash è diventato 3084d4c5a7659a2f3636b6079636b081b7ab1b6fca6a5e691c3afdd160959687, mentre prima era 2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893. Il file è cresciuto di due byte e l'impronta è cambiata con lui. Ripristina il file originale dalla tua copia prima di continuare.

Errori comuni

Variabili e percorsi senza virgolette. cat $file si rompe con i nomi che contengono spazi. Metti le virgolette per abitudine, e toglile solo quando sai che il valore non può contenere spazi o caratteri jolly.

Sovrascrivere un file di input con >. sort d.txt > d.txt svuota il file prima che sort lo legga. Scrivi su un file nuovo, controlla l'output, poi spostalo al suo posto.

Fidarsi di un numero di campo. $(NF-3) ha funzionato sul log di esempio. Prova la posizione del campo su qualche riga reale prima di costruirci sopra un report.

La modalità rigorosa che si ferma su un "nessuna corrispondenza" valido. Come visto sopra, grep che restituisce 1 termina una pipeline con pipefail. Decidi se per il tuo script nessuna corrispondenza è un errore, poi gestiscila di proposito.

Un loop che riporta lo stato sbagliato. L'hashall.sh qui sopra calcola l'hash di ogni file regolare. Quando però l'ultimo nome nella cartella è una sottocartella, lo script stampa tutti gli hash e poi esce con stato 1. Lo stato viene dal test finale [ -f ], che è fallito. La forma esplicita con if fa lo stesso lavoro ed esce con 0:

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

In una cartella di prova con una sottocartella come ultima voce, questa versione ha stampato l'hash di ogni file ed è uscita con stato 0. Anche la versione con && esce con stato 1 su una cartella vuota, quindi conviene partire dalla forma con if.

Eseguire script che non hai scritto tu. Uno script può cancellare o inviare file con la stessa facilità con cui ne calcola l'hash. Leggi prima tutto il file, controlla ogni comando che chiama ed eseguilo su una copia della cartella.

Per andare oltre

Leggi le sezioni del GNU Bash Reference Manual su pipeline, redirezioni, espansione dei parametri di shell e builtin set. Coprono quasi tutto ciò che hai visto qui. Usa la parte sugli stati di uscita come riferimento quando uno script si comporta in modo inatteso.

Poi leggi il manuale di GNU grep. Prova grep -i, grep -v e grep -E sul log di esempio e annota come ogni opzione cambia lo stato di uscita.

Il GNU Awk User's Guide spiega campi, pattern e array. Il contatore su una riga visto sopra è un piccolo esempio di cosa può fare awk. Quando ti viene naturale, scrivi un secondo pattern che conti gli accessi riusciti per utente.

Tieni corti i tuoi script. Uno script di dieci righe che sai spiegare riga per riga è più utile di uno di cento che hai copiato e non sai modificare. Quando uno script supera la lunghezza di una schermata, dividilo in funzioni o valuta un linguaggio con strutture dati più adatte al compito.

Fonti

Fonti

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

Altro su Linux e strumenti per sviluppatori

Linux e strumenti per sviluppatori

Permessi Linux: utenti, gruppi, chmod e umask

Proprietari e gruppi Linux, bit rwx, chmod, umask e setuid, con un lab che prova l'accesso come secondo utente e trova i permessi rischiosi sui file.

Base13 min di lettura

Linux e strumenti per sviluppatori

Chiavi SSH: come funzionano e come usarle in sicurezza

Come funzionano le chiavi host e utente di SSH, perché la scelta predefinita è Ed25519 con passphrase e come impostare un accesso con chiave e fare hardening su una VM di laboratorio tua.

Base11 min di lettura