Laboratori di hacking
Perché usare NotHoly per CTF e laboratori
Un modello che scrive l'exploit del tuo challenge e spiega perché funziona, triage statico del binario, e un solve che gira nel tuo lab, non nel nostro.
Crea una rete Docker senza accesso a internet, avvia OWASP Juice Shop al suo interno e pubblica una copia per il browser solo su 127.0.0.1. Verifica ogni passo.
Un lab è sicuro quanto lo sono le strade per entrarci e per uscirne. Se la tua app vulnerabile può raggiungere internet, può contattare posti che non volevi toccare. Se ascolta su ogni interfaccia della tua macchina, chiunque sia sullo stesso Wi-Fi può raggiungerla. Docker ti dà il controllo di entrambe le strade con pochi comandi. Questa guida usa OWASP Juice Shop come bersaglio e li percorre uno per uno.
Creerai una rete senza via d'uscita, ci avvierai Juice Shop, chiamerai l'app da un secondo container e dimostrerai che la rete non raggiunge internet. Poi eseguirai una copia separata per il browser, pubblicata solo sull'interfaccia di loopback, così niente sulla tua rete locale può vederla. L'isolamento è il primo controllo di sicurezza di qualsiasi lab, e ogni esercizio successivo, dalla scansione agli esercizi di sfruttamento, dipende da questo.
Se non hai ancora eseguito Juice Shop, comincia da OWASP Juice Shop con Docker. Questo articolo dà per note le basi sui container di quella guida.
Un lab di sicurezza esegue software fatto per essere rotto. Juice Shop ha falle volute, ed è proprio il suo scopo. Il rischio non è l'app in sé, ma quello che succede quando una falla, una configurazione sbagliata o un errore di battitura danno all'app, o a uno strumento che le lanci contro, una strada verso qualcos'altro.
Tre casi mostrano come può andare storto:
-p 3000:3000. Docker allora accetta connessioni su ogni interfaccia dell'host, quindi gli altri dispositivi della tua rete possono raggiungere l'app vulnerabile.L'isolamento copre ciascun caso. Una rete interna non ha rotte verso l'esterno. Una porta legata al loopback non ha strade attraverso la LAN. Una rete dedicata tiene i container del lab separati dai tuoi altri progetti. Nessuno di questi passi è difficile. Sono facili da dimenticare quando vai di fretta, per questo la guida te li fa verificare uno a uno.
Ti servono tre idee perché i comandi abbiano senso.
Docker collega i container a reti virtuali. Il driver predefinito è bridge. Ogni container su una rete bridge ottiene un proprio indirizzo IP su una subnet privata. Su Linux il bridge sta sull'host stesso. Su Windows e macOS, Docker Desktop esegue il motore dentro una macchina virtuale Linux, quindi il bridge sta dentro quella VM.
I container sul bridge predefinito si raggiungono per indirizzo IP ma non per nome. Una rete bridge che crei tu con docker network create risolve i nomi dei container via DNS, con un resolver integrato in Docker. Per questo il container di strumenti di questa guida può chiamare http://juice:3000 invece di dipendere da un IP che cambia dopo un riavvio.
Quando crei una rete con --internal, Docker ne limita l'accesso esterno. La documentazione di Docker lo descrive così: i container su una rete interna possono parlare tra loro, ma non con ciò che sta all'esterno, internet compresa. Lo vedrai al passo 4, quando la risoluzione di un host esterno fallisce.
Per impostazione predefinita, la porta di un container non è pubblicata verso altre macchine. L'opzione -p o --publish cambia le cose. Mappa un indirizzo e una porta dell'host su una porta del container, e l'indirizzo che scegli decide quali interfacce accettano connessioni.
-p 3000:3000 ascolta su tutte le interfacce dell'host, compreso il tuo indirizzo LAN. -p 127.0.0.1:3000:3000 ascolta solo sul loopback, quindi possono connettersi solo i programmi sulla stessa macchina. La documentazione di Docker sulla pubblicazione delle porte mostra esattamente questa forma. La differenza è un prefisso, e pesa.
Il lab ha cinque parti:
juicenet, una rete interna senza rotte verso l'esterno.juice, Juice Shop collegato solo a juicenet, senza porte pubblicate.curlimages/curl su juicenet che chiama juice per nome.example.com, cosa che dovrebbe fallire.juice-local, una seconda istanza di Juice Shop pubblicata su 127.0.0.1:3000 per il tuo browser.Il container juice è quello che isoli. juice-local è una copia di comodo per navigare. Gira sulla rete predefinita, quindi può raggiungere internet. Tienilo presente e non inserire nell'app credenziali reali o dati personali.
Ti serve:
docker info dovrebbe stampare i dettagli del server senza errori. Sulla macchina usata per questa guida, Docker Engine 29.8.2 girava su Windows 11 tramite Docker Desktop.-w tra apici singoli richiedono un quoting diverso.Controlla i nomi prima di sceglierli. Altri progetti sulla tua macchina potrebbero già usare nomi come juice o juicenet. Esegui prima questo:
docker ps -a --format '{{.Names}}'
docker network ls --format '{{.Name}}'Se uno dei due elenchi mostra juice, juice-local o juicenet, scegli altri nomi o rimuovi prima quelli vecchi. Non cancellare niente che non hai creato tu.
Controlla anche l'immagine che stai per eseguire. Juice Shop pubblica le etichette di versione sulla sua immagine:
docker image inspect bkimminich/juice-shop --format '{{index .Config.Labels "org.opencontainers.image.version"}}'Questo comando fallisce finché l'immagine non è in locale, perché è il primo docker run a scaricarla. Sulla macchina usata per questa guida, l'immagine ha riportato la versione 20.3.0. La tua potrebbe riportarne un'altra, quindi annotala.
docker network create --internal juicenetDocker stampa l'ID della nuova rete. Il tuo sarà diverso. Questo è ciò che ha stampato sulla macchina di prova:
44a865311b8b0df7808f81dccd11f4b50ca38800d7a38a6d17bc80fc104dadf9Conferma che il flag ha avuto effetto:
docker network inspect juicenet --format '{{.Internal}}'trueSe l'output è false, hai creato la rete senza --internal. Rimuovila con docker network rm juicenet e ricreala. Non andare avanti finché non vedi true, perché tutte le prove successive danno per attivo il flag.
docker run -d --name juice --network juicenet bkimminich/juice-shopOgni opzione ha un compito. -d esegue il container in background. --name juice gli dà un nome DNS che gli altri container su juicenet possono risolvere. --network juicenet lo collega solo a quella rete interna. Non c'è -p, quindi il container non pubblica porte verso l'host.
Docker stampa l'ID del container:
d9f182bbbec2dc8db7dcde02f3e94b054274b6cf8addb1a5f994e9ee23ddebf8L'app all'interno ascolta sulla porta 3000. Juice Shop ha bisogno di un po' di tempo per partire dopo che il container è su, quindi non provare subito. Usa un ciclo che controlla se un container di strumenti riceve un HTTP 200:
for i in $(seq 1 30); do
s=$(docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000)
echo "try $i: $s"
[ "$s" = "200" ] && break
sleep 3
doneMentre l'app parte, il ciclo stampa 000, che significa che curl non è riuscito a connettersi. Sulla macchina di prova ha stampato 000 quattro volte e poi 200 al quinto tentativo. A te potrebbero servirne di più o di meno. Il ciclo si arrende dopo 30 tentativi. Se non vedi mai un 200, cerca errori in docker logs juice.
docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000200Leggi ogni parte. --rm cancella il container di strumenti quando esce. --network juicenet mette lo strumento sulla stessa rete interna dell'app. curlimages/curl è una piccola immagine che contiene curl. http://juice:3000 usa il nome del container, che Docker risolve dentro juicenet. Il flag -s nasconde la barra di avanzamento, -o /dev/null scarta il corpo della risposta e -w stampa solo il codice di stato.
Questo è lo schema che riuserai. Uno scanner, un proxy o uno script si unisce alla rete del lab e punta al container per nome. Sul tuo host non serve nessuna rotta verso l'app.
Il tag dell'immagine qui non è fissato, quindi Docker scarica quello che oggi è latest. Per un lab che pensi di ripetere, fissa il tag dopo averlo provato.
Una prova riuscita significa qualcosa solo se le sta accanto una prova fallita. Il passo 3 ha mostrato che il container di strumenti raggiunge l'app, quindi la rete funziona. Ora controlla l'altra direzione:
docker run --rm --network juicenet curlimages/curl -sS -m 5 https://example.com; echo "exit=$?"curl: (28) Resolving timed out after 5000 milliseconds
exit=28Il flag -m 5 limita la richiesta a cinque secondi. -sS nasconde la barra di avanzamento ma lascia visibili gli errori. Il codice di uscita 28 è il codice di timeout di curl. L'errore avviene nella risoluzione dei nomi: la ricerca di example.com non riceve mai risposta, quindi curl non apre mai una connessione. È quello che ti aspetti da una rete senza rotte verso l'esterno.
Questo è il risultato che vuoi. Il lab raggiunge i propri container e non raggiunge l'esterno. Salva l'output dei passi 3 e 4 negli appunti del lab con la data e la versione di Docker. Quella traccia ti aiuta quando torni e qualcosa è cambiato.
Il tuo browser non può risolvere il nome juice, e il container juice non pubblica porte, quindi dall'host non lo raggiungi. Per navigare, avvia una seconda copia e pubblicala sul loopback:
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shopL'opzione -p 127.0.0.1:3000:3000 mappa la porta 3000 dell'interfaccia di loopback sulla porta 3000 dentro il container. Questa copia usa il bridge predefinito, non juicenet, quindi non è isolata da internet. È un compromesso voluto per una copia da navigazione. Se ti serve qualcosa di più restrittivo, devi progettare e provare una configurazione separata.
La copia impiega un momento a partire. Usa lo stesso tipo di ciclo di prima, puntato sull'host:
for i in $(seq 1 30); do
s=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/)
echo "try $i: $s"
[ "$s" = "200" ] && break
sleep 3
doneSulla macchina di prova la copia ha risposto 200 al decimo tentativo. Ora conferma dove è legata la porta:
docker port juice-local3000/tcp -> 127.0.0.1:3000Passa al lato LAN. Trova l'indirizzo IPv4 della tua macchina sulla rete locale con ipconfig su Windows o ip addr su Linux. Poi richiedi l'app a quell'indirizzo:
curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://YOUR-LAN-IP:3000/Sostituisci YOUR-LAN-IP con il tuo indirizzo. Sulla macchina di prova ha stampato 000 e curl è uscito con codice 7, che significa che non è riuscito a connettersi. La stessa richiesta a 127.0.0.1 ha restituito 200. Il senso del bind sul loopback sta tutto in questa differenza.
Ora apri http://127.0.0.1:3000 nel browser. Dovresti vedere la home di Juice Shop. Usa l'app solo da quella stessa macchina.
Esegui docker ps per vedere i due container fianco a fianco:
docker ps --filter name=juice --format 'table {{.Names}}\t{{.Ports}}'NAMES PORTS
juice-local 127.0.0.1:3000->3000/tcp
juice 3000/tcpLa riga di juice mostra 3000/tcp senza indirizzo dell'host. La porta esiste dentro la rete ma non è mappata sull'host. La riga di juice-local mostra il binding sull'host.
Ferma e rimuovi il lab. I container vanno rimossi prima della rete, perché Docker si rifiuta di rimuovere una rete che ha ancora container collegati:
docker rm -f juice juice-local && docker network rm juicenetjuice
juice-local
juicenetDocker stampa i nomi che ha rimosso. Poi conferma che non è rimasto niente:
docker ps -a --filter name=juice --format '{{.Names}}'
docker network ls --filter name=juicenet --format '{{.Name}}'Nessuno dei due comandi dovrebbe stampare niente. I filtri trovano anche parti di un nome, quindi leggi l'output prima di considerarlo completo e rimuovi solo ciò che hai avviato tu. Nota anche che --rm pulisce solo i container di strumenti usa e getta. I container avviati con -d restano finché non li rimuovi, ed è per questo che esiste questo passo.
Pubblicare senza un indirizzo. -p 3000:3000 ascolta su ogni interfaccia dell'host. Usa il prefisso 127.0.0.1: per qualsiasi app del lab che apri dalla stessa macchina, e controlla il risultato con docker port.
Dimenticare --internal. La rete funziona senza il flag e niente ti avvisa. Esegui docker network inspect juicenet --format '{{.Internal}}' e cerca true.
Collegare l'app alla rete predefinita. Un container avviato senza --network si unisce al bridge predefinito, che raggiunge l'esterno. Se l'app deve restare isolata, indica la rete ogni volta.
Fidarsi di una prova fallita. Un errore al passo 4 potrebbe venire dal DNS, da un proxy, da una VPN o da un errore di battitura. Il passo 3 è il controllo che mostra che la prova misura la cosa giusta.
Usare il nome dall'host. Il nome juice si risolve solo per i container su juicenet. Il tuo browser non può usarlo. Per questo esiste la copia da browser.
Lasciare i container in esecuzione. Un container in background continua a usare risorse e tiene legata la sua porta. Chiudi ogni sessione con il passo di pulizia ed esegui docker ps subito dopo.
L'opzione di rete interna è un controllo, e ha dei limiti.
Non impedisce a uno strumento su juicenet di raggiungere ogni altro container su quella rete. I container sulla stessa rete possono parlare tra loro, ed è ciò che fa funzionare il passo 3. Tieni i servizi estranei fuori dalla rete del lab.
La pubblicazione sul loopback ti protegge dalle altre macchine della tua LAN. Non ti protegge dai programmi già in esecuzione sul tuo computer, perché anche quelli possono connettersi a 127.0.0.1.
La prova controlla un solo nome e un solo host esterno. Non dimostra che ogni percorso in uscita sia chiuso. Il comportamento di Docker può variare tra Docker Desktop, motori Linux e configurazioni rootless, quindi ripeti i controlli sulla macchina che usi davvero.
Infine, questo lab mostra solo che l'isolamento funziona. Non mostra che Juice Shop sia sicuro da esporre. Non lo è, ed è il motivo per tenerlo isolato.
Quando il lab funziona, qualche piccola modifica lo rende più utile:
juicenet, per esempio un database, e conferma che l'app lo raggiunge per nome. Così si costruiscono i lab con più container.juicenet è un buon primo bersaglio perché controlli ogni container che ci sta sopra.Laboratori di hacking
Un modello che scrive l'exploit del tuo challenge e spiega perché funziona, triage statico del binario, e un solve che gira nel tuo lab, non nel nostro.
Laboratori di hacking
Dai una struttura a un report di pentest e scrivi un finding che altri possano riprodurre, con prove, impatto, un vettore CVSS v4.0 e una correzione.
Laboratori di hacking
Costruisci un home lab che hai il diritto di attaccare: un file di perimetro, porte Docker legate al loopback, una rete isolata, un quaderno di appunti e uno smontaggio pulito.