- Filosofia Unix
- La shell
- Esempio di utilizzo di backpipe per una connessione bidirezionale
- Chat con netcat attraverso firewall
- Reverse shell con netcat
- SSH port forwarding
- Ogni programma deve svolgere un compito solo, e deve farlo bene.
- Ci si aspetta che l'output di un programma sia l'input di un altro programma, per cui: non sporcare l'output con dati estranei!
- Scrivi programmi che gestiscano flussi di testo perché questa è un'interfaccia universale.
- La modularità si realizza con sottili strati di colla tra le componenti, vanno cioè evitate astrazioni eccessive: il contrario della programmazione ad oggetti.
L'input da tastiera dell'utente viene interpretato dalla shell, solitamente la shell interattiva è la bash e si presenta così:
$ _
Alcuni semplici comandi che useremo in seguito. Le righe che iniziano con $
sono i comandi, quelle senza sono le righe dei risultati:
$ echo Hello world
Hello world
echo
scrive su stdout la stringa passata come argomento.
$ cat /etc/debian_version
9.0
cat
scrive su stdout il contenuto dei file passati come argomenti.
$ echo "questa frase contiene 5 parole" | wc
1 5 31
wc
(word count) conta in numero di righe, parole e caratteri dell'input. Sì,
il numero di caretteri dovrebbe essere 30 ma echo
aggiunge il ritorno a capo;
quando si vuole evitare questo effetto collaterale si usa printf
invece di
echo
.
$ nc -l -p 12345
Netcat: nc
ha molti utilizzi, in questo caso è usato in modalità server,
rimane cioè in ascolto sulla porta tcp 12345 in attesa di connessioni da parte
di un client. Come al solito ha all'avvio ha i canali stdin, stdout e stderr
collegati al terminale.
Nell'esempio abbiamo usato la versione tradizionale di nc
che richiede lo
switch -p
per la porta su cui ascoltare, la versione openbsd invece lo
considererebbe un errore: in questo caso avremmo scritto nc -l 12345
.
$ nc localhost 12345
Questo è il client corrispondente: si connette al server in ascolto su
localhost porta 12345. Ovviamente se il client si trova su un altra macchina
rispetto al server al posto di localhost
si usa l'hostname (o indirizzo IP)
del server).
Ogni processo Unix è connesso di default a tre canali (stream):
- standard input o stdin
- standard output o stdout
- standard error o stderr
Di default i tre canali sono connessi al terminale (ad esempio /dev/tty
)
quindi ricevono l'input dalla tastiera mentre lo stdout è il terminale così
come lo è stderr; cioè anche gli errori vengono scritti sul terminale.
Unix considera ogni risorsa di I/O (input/output) come un file, quindi anche questi canali vengono considerati come tali.
Ad ogni file aperto Unix fa corrispondere un descrittore di file numerico: file descriptor.
I file descriptor corrispondenti a stdin, stdout e stderr sono rispettivamente 0, 1 e 2.
Ad altri eventuali file aperti dal programma vengono associati altri file descriptor, i.e. altri numeri.
Gli operatori >
e <
permettono di redirigere l'I/O.
comando > file
Lo stdout di comando
viene rediretto su file
$ echo 42 > risposta_alla_domanda_fondamentale
In questo caso 42
invece di essere scritto su stdout viene scritto sul file
risposta_alla_domanda_fondamentale
(se il file esiste viene sovrascritto e se
non esiste viene creato).
comando < file
Il contenuto di file
viene passato a comando
via stdin.
$ md5sum < risposta_alla_domanda_fondamentale
50a2fabfdd276f573ff97ace8b11c5f4 -
In questo caso md5sum
riceve via stdin il numero 42.
Notare che è diverso eseguire:
$ md5sum risposta_alla_domanda_fondamentale
50a2fabfdd276f573ff97ace8b11c5f4 risposta_alla_domanda_fondamentale
In questo caso md5sum
riceve come argomento il nome del file
risposta_alla_domanda_fondamentale
, lo apre (il file descriptor da cui legge
sarà 3, non 0!) e calcola l'MD5 del contenuto (42).
Riprendendo l'esempio precedente:
host-A | host-B |
---|---|
$ nc -l 5757 |
$ nc host-A 5757 |
Ora l'input di A viene ricevuto dal suo nc
e passato a host-B che lo scrive sul proprio terminale.
Analogamente quello che viene scritto su host-B viene visualizzato sul terminale di host-A.
Importante è il concetto di pipe, implementato dall'operatore |
.
La pipe è lo strumento che connette l'output di un programma all'input di un altro.
$ md5sum risposta_alla_domanda_fondamentale | wc
1 2 69
In questo caso wc
riceve come stdin la stringa
50a2fabfdd276f573ff97ace8b11c5f4 risposta_alla_domanda_fondamentale
e ne
restituisce il numero di righe, parole e caratteri.
La pipe è unidirezionale: quello che riceve su stdin (dal comando a sinistra) lo inoltra (stdout) al comando a destra che lo riceve come proprio stdin.
Per ottenere le connessioni bidirezionali che vedremo dopo è necessario usare le named pipe. Le named
pipe sono file particolari di tipo FIFO: ricordiamoci che per Unix tutto è (dovrebbe) essere
un file. Per creare una named pipe si usa il comando mkfifo nomepipe
o
equivalentemente mknode -p nomepipe
.
$ mkfifo f
$ ls -l f
prw-r--r-- 1 debe debe 0 mag 23 10:04 f
La lettera 'p' indica che il file f
è una named pipe (se fosse un file
regolare sarebbe '-', se fosse una directory 'd', …).
$ mkfifo backpipe
$ nc -l 12345 < backpipe | nc www.google.com 80 > backpipe
Collegando un browser a http://localhost:12345
si ottiene una risposta da
google.
Il primo nc
si mette in ascolto sulla porta 12345 dalla quale riceverà input
dal client che verrà replicato su stdout, questo verrà passato, via |
,
allo stdin del secondo nc
.
Contestualmente imposta il suo stdin su backpipe
(cioè il file
descriptor 0 punta a backpipe
). L'input ricevuto viene inoltrato al client
connesso sulla porta 12345.
Riceve in input, attraverso |
, l'output del primo nc
e provvede ad
inoltrarlo a www.google.com:80; quando riceve la risposta da google la scrive
su stdout; quest'ultimo, intercettato da >
, viene rediretto su backpipe
.
---> tcp[nc1]out ---> in[|]out ----> in[nc2]tcp ---------v
browser www.google.com:80
<--- tcp[nc1]in <--- out[backpipe]in <--- out[nc2]tcp <---
Quindi la prima pipe, |
, permette la connessione browser→google mentre la backpipe
quella google→browser.
L'ipotesi è che i due host siano all'interno delle rispettive LAN in cui il firewall blocca connessioni entranti e permette ai client di collegarsi in http e https.
Abbiamo però a disposizione un host esterno, host-C, che accetta connessioni.
host-A | host-C | host-B |
---|---|---|
$ mkfifo p |
||
$ cat p | nc -l -p 80 | nc -l -p 443 > p |
||
$ nc host-c 80 |
$ nc host-c 443 |
A questo punto host-A e host-B possono chattare passando attraverso host-C.
Sull'host remoto (remotehost):
$ sudo -i
# rm -f /tmp/p
# cat /tmp/p | /bin/sh -i 2>&1 | nc -l 9898 > /tmp/p
Sull'host locale:
$ whoami
debe
$ nc remotehost 9898
# whoami
root
Partiamo da nc
.
nc
riceve input sulla porta 9898 (attenzione che non è lo stdin
) e lo manda
in output (stdout
) e >
lo passa in input alla named pipe.
Notare che quello che nc
riceve, e passa a p
, è l'input dell'utente che si
è collegato con il suo client alla porta 9898 sull'host remoto.
Il cat
legge quello che c'è in p
(quindi come dicevamo si tratta dei
comandi dell'utente) e butta tutto in stdout
; ci pensa poi il |
che segue a passare
questi comandi alla shell interattiva (la shell si può usare anche in
modalità non interattiva per eseguire script) per questo c'è il -i
; inoltre
c'è anche la redirezione degli errori 2>&1
su stdout
che spiego dopo.
La shell, dicevamo, riceve i comandi dell'utente, i risultati li scrive su
stdout
che tramite il |
che segue finiscono in input (questa volta sì
stdin
) a nc
che provvede a passarli al client connesso via tcp.
Se non ci fosse la redirezione stderr
su stdout
(2>&1
) il client non
riceverebbe gli errori e nemmeno il prompt dei comandi della shell, ma
funzionerebbe lo stesso. Quello che succede nel dettaglio è che quando
viene invocato /bin/sh
il suo stderr
viene fatto puntare a stdin
dopodiché stdin
viene passato attraverso la pipe |
a nc
.
stderr ----> stdin -----> nc
SSH normalmente realizza la funzionalità di shell remota attraverso una connessione sicura tra due host. SSH è anche in grado di usare questo canale per trasportare altro tipo di traffico fra e attraverso i due host. Si tratta delle funzionalità di port forwarding e ce ne sono tre tipi:
- Local port forwarding
- Remote port forwarding
- Dynamic port forwarding
Il local port forwarding permette di connettere il computer locale ad un server remoto passando per il tunnel SSH.
Può essere necessario quando il servizio che vogliamo utilizzare consente l'accesso solo dal server ssh e non dalla nostra macchina (si usa spesso per connessioni a database che hanno ACL basate su indirizzi IP).
In altri casi è utile per far passare sul canale sicuro un servizio che viaggerebbe in chiaro.
ssh -L localport:host:hostport server
La porta locale localport
apre una connessione verso
host:hostport
passando attraverso il tunnel ssh e uscendo da
server
.
Per questo esempio usiamo http://ipecho.net/plain che ci dice da quale
indirizzo provenie la nostra richiesta http, curl
è un client http.
$ curl http://ipecho.net/plain
62.101.86.50
Questo è in nostro indirizzo in condizioni normali.
$ ssh -fNL 5757:ipecho.net:80 62.101.86.55
Ora abbiamo la porta locale 5757 in listen: quando un client si collega la
connessione viene inoltrata, attraverso il tunnel e quindi attraverso
62.101.86.55, verso il server http ipecho.net
.
Il flag importante è -L
. -N
fa in modo che ssh attivi la connessione
verso 62.101.86.55 senza aprire la shell remota e -f
mette il processo
in background: insomma rimaniamo sulla macchina locale anche dopo aver fatto
partire ssh.
$ curl --header "HOST: ipecho.net" http://localhost:5757/plain
62.101.86.55
Qui curl
collegandosi alla porta 5757 sulla macchina locale, raggiunge
effettivamente ipecho.net
che questa volta ci vede arrivare con indirizzo
62.101.86.55
.
Nella richiesta http è necessario aggiungere l'header Host
altrimenti il
server ipecho.net non ci risponde bene.
In questo caso è il server ssh che può connettersi ad un host remoto attraverso il client ssh.
Utile ad esempio se abbiamo un servizio sul nostro laptop non raggiungibile dall'esterno: ci si connette ad un server pubblico su internet che inoltrerà le connessioni al nostro servizio.
ssh -R port:host:hostport server
Sul server
la porta port
attende connessioni che verranno inoltrate a
host:hostport
attraverso il tunnell ssh, quindi uscenti dalla macchina
locale.
Ad esempio sul mio laptop ho un server web ma non ho un indirizzo pubblico o il firewall blocca le connessioni entranti.
Ho accesso al server pubblico example.org
via ssh.
$ ssh -R 8080:localhost:80 example.org
Le connessioni a example.org
sono inoltrate al mio laptop, localhost
in
questo caso è relativo alla macchina locale; posso così pubblicare il mio sito come http://example.org:8080
.