Subsections of Incontri

13 Dicembre 2024

Slide PDF

Preambolo

cosa ci serve:

  • basi di web security (requests)
  • conoscenza di base di SQL (ma anche no)
  • creatività

Questa lezione si può seguire da Windows o Linux (ma sempre meglio installare Linux 🙏).

a cosa mi serve SQL?

Alcuni di voi avranno visto SQL in sè per sè, ma come lo integro in un applicazione e per cosa lo uso?

Use cases ovvi:

  • conservare dati strutturati ->
    • credenziali applicazioni web 🔥?
    • dati personali di utenti?

Chiaramente per un attaccante un database è sempre una superficie d’attacco notevole.

come interagisce un applicazione con il DB?

In qualche modo la mia applicazione farà query al database sottostante.

Possiamo immaginarci qualcosa del genere:

user_input = ... #prendiamo input dall'utente
query = "SELECT * FROM users WHERE id = " + user_input
cursor.execute(query)

Come al solito, il primo approccio che prendiamo è spesso sbagliato. Come posso rompere un codice del genere?

Se avete voglia di testare un po’ (e dovreste) provate questo sito.

cosa non va?

Guardando qualsiasi applicativo conviene sempre ragionare partendo da dove abbiamo controllo, ovvero il nostro input. Dove andrà a finire ciò che mandiamo? Che tipo di restrizioni sono imposte su ciò che mandiamo?

Ragionando su questi binari, ci rendiamo presto conto che

  • Possiamo mandare quello che ci pare
  • andrà a finire direttamente nella query

Nulla a questo punto ci vieta di mandare comandi, ovvero parole che SQL interpreta come codice : possiamo iniettare (injection) SQL a nostro piacimento!

Abbiamo una SQL injection, e questo vuol dire che siamo liberi di leggere e scrivere nel DB, in modo (spesso) arbitrario.

un esempio più complesso (e utile)

Spesso abbiamo meno controllo sul nostro input : o è filtrato o è piazzato in modo sconveniente se vogliamo rompere l’applicazione.

Provate a risolvere questa challenge di OliCyber (hint hint : control characters).

definizione: injection

Quando un programma interpreta il nostro input, che dovrebbe essere letterale ed inerte, come codice. Spesso richiede un qualche escape character o sequence.

tangente : come si fa un app sicura?

Ci sono modi e modi di fare query:

  • I modi giusti e sicuri 🥱 :
    • query parametrizzate : uso dei placeholder per rappresentari i miei valori nella query :
    cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
  • ORM (Object Relational Mapping) : Uso librerie per astrarre le query (SQLAlchemy)

Ricordatevi che in questo modo vi state semplicemente affidando al codice di altre persone, che spesso è ugualmente rotto, però meglio di niente

e se non vedo tutto l’output?

é molto raro che i server connettano direttamente gli utenti al DB senza fare qualche altro calcolo in mezzo.

Forse l’injection è in un endpoint che restituisce solo un valore numerico, oppure booleano per la logica dell’applicazione.

O ancora, immaginiamoci di avere un injection che ci permette di sovvertire un endpoint per vedere i dettagli di un utente, in modo da poter vedere anche la sua password : magari la query che viene eseguita è giusta, ma il server prova a interpretarla secondo il formato che si aspetta (senza la password) e crasha, senza darci altre informazioni.

Ci troviamo davanti a un esempio di oracolo

definizione : oracolo

L’oracolo (dal latino oraculum) è un essere o un ente considerato fonte di saggi consigli o di profezie, un’autorità infallibile, solitamente di natura spirituale.

In cybersecurity, una funzione che ci permette di interrogare un programma ricevendo una risposta booleana (si o no).

Spesso nel mondo reale la risposta non è diretta, ma deriva dalla presenza o meno di un errore : error-based oracle

come si chiede un nome ad un oracolo?

Noi vogliamo una stringa, ma l’oracolo ci dà solo si e no! -> divide et impera

Chiediamo un carattere alla volta, testando con tutto l’alfabeto -> la prima lettera è [A-Z]?

Dopo qualche richiesta avremo il primo carattere, e procediamo così fino alla fine.

si può fare di meglio (meno richeste)?

Lasciato come esercizio al lettore

una challenge ad oracolo

Familiarizzate con le challenge ad oracolo risolvendo questa!

E se l’output non c’è?

A volte il server non ha bisogno di mandarci nulla indietro nel suo uso inteso (ex. aggiornare il proprio username). Come facciamo?

Forti del fatto che la giusta combinazione di 0 ed 1 possono rappresentare tutto il rappresentabile, non ci rimane che trovare un modo nuovo di ricavarli.

In diversi dialetti di SQL abbiamo delle simpatiche funzioni, che possiamo usare in congiunzione ad IF per ottenere oracoli :

  • SLEEP : blocca il DB per x secondi -> se la risposta del server è lenta, abbiamo un 1 altrimenti 0
  • Funzioni di read di risorse : alcuni server SQL permettono la lettura di risorse, sia locali che remote (protocolli HTTP o FTP) -> secondo voi come derivo un oracolo?

Queste sono alcuni casi comuni, ma la parte divertente è adattarsi alle circostanze in modi nuovi.

Non usate tool già fatti come sqlmap, non impararete molto e di sicuro non furbi quanto voi (aka non funzionano)

29 Novembre 2024

Parte 0: Accesso alla piattaforma

Parte 1: Network Basics

Wireshark

Se utilizzi Kali Linux, Wireshark è già preinstallato. Per altre distribuzioni, puoi installarlo con uno dei seguenti comandi:

sudo apt install wireshark         # Per Ubuntu/Debian/Kali
sudo dnf install wireshark         # Per Fedora
sudo pacman -S wireshark           # Per Arch Linux
sudo zypper install wireshark      # Per openSUSE
Challenge

  1. Installa Wireshark sul tuo computer e prova a catturare il traffico di rete mentre navighi su internet. Riesci a trovare il tuo traffico HTTP?
  2. Prova a fare ping ad un altro computer e cattura il traffico di rete con Wireshark. Cosa vedi?

Parte 2: Firewall su Linux

Il kernel Linux utilizza il sottosistema netfilter per filtrare il traffico di rete. Per configurare netfilter, i due strumenti principali sono:

  • iptables: uno strumento classico e ampiamente utilizzato.
  • nftables: una soluzione più recente e flessibile.

Durante questa lezione, ci concentreremo su iptables.

iptables

Comandi utili

iptables -L -n -v  # Mostra le regole del firewall
iptables -F        # Pulisce tutte le regole

iptables -P INPUT DROP      # Imposta la policy di default per il traffico in ingresso a DROP
iptables -P OUTPUT ACCEPT   # Imposta la policy di default per il traffico in uscita ad ACCEPT
iptables -P FORWARD DROP    # Imposta la policy di default per il traffico di transito a DROP


iptables -A INPUT -p tcp --dport 22 -j ACCEPT   # Consente il traffico TCP sulla porta 22
iptables -A INPUT -p tcp --dport 80 -j ACCEPT   # Consente il traffico TCP sulla porta 80

iptables -D INPUT -p tcp --dport 22 -j ACCEPT   # Rimuove la regola per la porta 22
iptables -D INPUT 1                             # Rimuove la prima regola nella catena INPUT

iptables -A INPUT -s x.x.x.x -j DROP            # Blocca il traffico proveniente dall'indirizzo IP x.x.x.x
iptables -A INPUT -s x.x.x.x -j REJECT          # Rifiuta il traffico proveniente dall'indirizzo IP x.x.x.x

iptables-save > /etc/iptables/rules.v4          # Salva le regole in un file
iptables-restore < /etc/iptables/rules.v4       # Carica le regole da un file

firewalld

FirewallD è un software di gestione del firewall per Linux che fornisce un interfaccia D-Bus per impostare regole di firewall.

E’ il firewall di default su distribuzioni di derivazione Red Hat come Fedora, CentOS e RHEL. In aggiunta a quanto già offerto da iptables, firewalld offre il concetto di zone, la cui gestione permette di semplificare la configurazione del firewall.

La sua interfaccia a riga di comando è firewall-cmd.

# Visualizza la zona attiva
firewall-cmd --get-active-zones

# Mostra le regole della zona corrente
firewall-cmd --list-all

# Visualizza le regole della zona 'public'
firewall-cmd --zone=public --list-all

# Aggiungi il servizio HTTP alla zona 'public'
firewall-cmd --zone=public --add-service=http

# Aggiungi il servizio HTTP in modo permanente
firewall-cmd --zone=public --add-service=http --permanent

# Riavvia il servizio firewalld
systemctl restart firewalld

# Blocca tutto il traffico in ingresso eccetto la porta 22
firewall-cmd --zone=public --set-target=DROP
firewall-cmd --zone=public --add-port=22/tcp

# Salva la configurazione attuale
firewall-cmd --runtime-to-permanent

Permanent vs Runtime

Le modifiche fatte con firewall-cmd sono, di default, temporanee (runtime). Per renderle permanenti, usa il comando:

firewall-cmd --runtime-to-permanent

Parte 3: VPN

Una VPN (Virtual Private Network) è un metodo per creare una connessione sicura tra due dispositivi su una rete pubblica.

OpenVPN

OpenVPN è un software open-source che opera a livello user-space, offrendo grande flessibilità, ma con prestazioni leggermente inferiori rispetto ad altre soluzioni.

WireGuard

WireGuard è una VPN open-source integrata direttamente nel kernel Linux, offrendo maggiore velocità e sicurezza grazie a impostazioni predefinite robuste.

WireGuard su Linux

WireGuard è supportato nativamente dai kernel moderni. Per configurarlo, è necessario installare wireguard-tools:

sudo apt install wireguard-tools     # Per Ubuntu/Debian/Kali
sudo dnf install wireguard-tools     # Per Fedora
sudo pacman -S wireguard-tools       # Per Arch Linux
sudo zypper install wireguard-tools  # Per openSUSE

[Interface]
PrivateKey=<chiave_privata>
Address=<ip_locale>
ListenPort=<porta>  # opzionale

[Peer]
PublicKey=<chiave_pubblica>
Endpoint=<ip_pubblico>:<porta>
AllowedIPs=<ip_remoti>

Comandi utili per WireGuard:

sudo wg-quick up <config_file>.conf     # Avvia la VPN
sudo wg-quick down <config_file>.conf   # Arresta la VPN

22 Novembre 2024

Parte 0: Accesso alla piattaforma

Parte 1: Network Basics

Per parlare di Web Security è necessario avere una base di conoscenza di come funzionano le reti di computer.

Parte 2: Web Security e attacchi alla sessione

Livello base

Livello facile

Livello medio

  • To be continued…

15 Novembre 2024

Preambolo

Durante questo incontro vedremo come funziona un buffer overflow, come sfruttarlo e una breve introduzione a pwntools.

Cosa ci serve:

  • gcc
  • gdb
  • python
  • pwntools
  • conoscenza basilare di C
  • voglia di imparare

Tutti i tool sono disponibili di default su Kali.

Su Ubuntu si installano con :

$ sudo apt install build-essential gdb python3 python3-pip python3-dev git libssl-dev libffi-dev gcc-multilib
$ pip install --break-system-packages pwntools
Comandi shell

Di solito i comandi shell nelle varie risorse online sono preceduti da un $, che rappresenta il prompt del terminale. Questo viene usato per:

  • indicare che il comando va eseguito come utente normale (ovvero non come root).
  • rendere il copia e incolla più difficile e costringere a modificare il comando prima di eseguirlo, riducendo il rischio di eseguire comandi pericolosi per errore.

Buffer Overflow

Ogni problema si può comprendere su diversi livelli di astrazione. Solitamente, ragioniamo su un programma C (o qualsiasi altro linguaggio) al livello del linguaggio stesso. Da questo punto di vista il programma non è altro che un insieme di variabili e funzioni, e tutta l’esecuzione parte dal main. Sappiamo però che ciò che esegue il nostro computer è un binario generato dal compilatore e dal linker -> cosa succede tra un’ astrazione e l’altra?

Digging deeper

Senza entrare nelle specifiche, concettualmente il nostro computer funziona in modo “lineare” consuma un “nastro” di codice, eseguendone le istruzioni. Questo differisce abbastanza dalla visione strutturata di un programma C, dove abbiamo diverse funzioni innestate che si chiamano fra di loro (o se stesse)

Come funziona una funzione?

Oltre al gioco di parole, è importante capire come funziona una funzione a basso livello.

Non esiste un concetto nativo di funzione per il computer esegue un programma : il programma è semplicemente un insieme (tendenzialmente) monolitico di istruzioni, da caricare in memoria (il nastro).

A un alto livello, cosa serve per implementare una funzione?

Dati :

  • Un modo di comunicare degli argomenti -> li carico (in memoria) da qualche parte
  • Un modo di comunicare il risultato -> lo carico da qualche parte…

Metadati :

  • Un modo di identificare una funzione -> un indirizzo in memoria
  • E infine… un modo di tornare alla funzione precedente! -> lo salvo prima della chiamata

D’ora in poi considereremo nello specifico l’architettura x86_32, i cui eseguibili sono facili da eseguire sulla maggior parte dei computer moderni

Un semplice programma-cavia

Per capire meglio come funzionano le chiamate a funzione, consideriamo il seguente programma:

#include <stdio.h>

void impossibile_da_chiamare() {
    printf("Come hai fatto???\n");
}

void funzione_normalissima() {
    char buffer[64];
    printf("Come ti chiami? ");
    gets(buffer);  // Non usarmi!
    printf("Ciao %s!!\n", buffer);
}

int main() {
    printf("Qualche professore amerebbe questo programma\n");
    funzione_normalissima();
    return 0;
}

Il programma è molto semplice, chiede il nostro nome e lo stampa a schermo. Da notare la funzione gets, che è molto pericolosa e non dovrebbe essere usata mai in un programma reale.

Se provate a compilare questo programma normalmente, il compilatore si arrabbierà (con buone ragioni). Perchè?

$ gcc dim.c

dim.c: In function ‘funzione_normalissima’:
dim.c:10:5: error: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
   10 |     gets(buffer);  // Non usarmi!
      |     ^~~~
      |     fgets

1. Che succede se passo più di 64 caratteri?

A prima vista, il programma sembra funzionare correttamente. Notiamo però che la variabile buffer è grande solo 64. Cosa succede se passiamo più di 64 caratteri?

Facciamolo!

$ ./dim
Qualche professore amerebbe questo programma
Come ti chiami? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Ciao AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!!
Errore di segmentazione (core dump creato)

Il programma crasha! Perchè?

1.a Strumenti utili

  • gdb : debugger, ci aiuterà a capire meglio cosa fa il nostro programma e a capire perchè crasha
  • pwntools : libreria di python, ci permette di interagire in modo programmatico con il nostro binario

Entrambi installati di default su Kali!

2. Cosa sta succedendo?

Proviamo ad analizzare il problema con gdb:

$ gdb ./dim
(gdb) run

Inseriamo il nostro input e vediamo cosa succede:

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

L’errore di segmentazione (SIGSEG) è il più classico, ma cosa significa 0x41414141 in ???

Normalmente gdb ci mostra sia l’indirizzo di memoria in cui si è verificato l’errore, sia la funzione in cui si è verificato.

  • 0x41414141 è un indirizzo di memoria, ma cosa rappresenta?
  • ?? indica che gdb non sa cosa ci sia in quell’indirizzo di memoria

Il programma ha quindi provato ad eseguire una “funzione” all’indirizzo 0x41414141. E’ un caso?

Se proviamo a convertire 0x41414141 in ASCII otteniamo “AAAA”, che è esattamente una parte del nostro input.

Possiamo quindi dedurre che il programma ha provato ad accedere ad una cella di memoria che è stata sovrascritta con il nostro input.

3. Come sono implementate le chiamate a funzione in x86_32?

Non entreremo troppo nei dettagli (layout dello stack e memoria), ciò che ci basta sapere per ora è questo: durante una chiamata a funzione, il programma salva i metadati della funzione chiamante nello stack, e poi salta (jump incondizionato) alla funzione chiamata.

Lo stack è una struttura dati a pila, ovvero una struttura dati in cui l’ultimo elemento inserito è il primo ad essere rimosso, quindi, al momento di chiamare la funzione, i metadati della funzione chiamante si trovano in cima allo stack.

Durante l’inizializzazione della funzione chiamata, vengono pushati sullo stack i dati della funzione chiamata (ovvero le variabili locali) e poi viene eseguito il codice della funzione.

Quindi:

I metadati di una chiamata a funzione sono conservati in modo adiacente ai dati locali della funzione chiamante; per ora immaginiamoci che siano situati negli indirizzi di memoria appena successivi.

Inoltre ricordiamoci che In C accedere ad un elemento in un array (array[i]) vuol dire accedere ad un elemento in un indirizzo = array + i * sizeof(type).

Ad esempio, buffer[65] nonostante sia “fuori dal buffer” è un modo per accedere al 65° byte del buffer, che però non è del buffer stesso ma di un’altra variabile in memoria (o, in questo caso, dei metadati della funzione chiamante).

Utilizzando queste due informazioni, possiamo capire cosa è successo:

  1. Abbiamo sovrascritto il buffer con più di 64 caratteri;
  2. Abbiamo sovrascritto i metadati della funzione chiamata;
  3. Nel momento in cui la funzione chiamata tenta di ritornare alla funzione chiamante (return), il program counter (PC) si sposta all’indirizzo che abbiamo scritto come 65° byte del buffer (in realtà non è detto sia proprio il 65° byte, in quanto il compilatore può inserire padding tra le variabili).

Possiamo quindi controllare il PC, ergo possiamo eseguire una funzione qualsiasi (con alcuni limiti, ma per ora non ci interessano)!

4. Cosa eseguiamo?

Proviamo, ad esempio, a chiamare la funzione impossibile_da_chiamare.

Sappiamo che ogni funzione è caricata in memoria ad un indirizzo specifico dello spazio di indirizzamento del processo . Come facciamo a sapere quale è l’indirizzo di impossibile_da_chiamare?

Riapriamo gdb:

gdb ./a.out

e chiediamo l’indirizzo della funzione:

(gdb) x *impossibile_da_chiamare

oppure

(gdb) info functions
Address Space Layout Randomization

Per motivi di sicurezza di solito gli indirizzi di memoria cambiano tra un esecuzione e l’altra (vedi ASLR). Possiamo però disabilitarlo temporaneamente con:

$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

5. Come scriviamo un indirizzo arbitrario in input?

Da terminale non possiamo scrivere un indirizzo arbitrario, in quanto non possiamo scrivere caratteri non ASCII. Per questo è necessario un aiuto da parte di python3.

$ python3 -c "print(b'A'*64 + b'\x01\x02\x03\x04')" | ./dim

in questo modo stiamo passando al programma dim 64 caratteri A seguiti da 0x04030201 (in little-endian, ovvero con gli indirizzi in ordine inverso rispetto a come li scriviamo).

In alternativa, possiamo usare pwntools:

r = process("./dim")
r.sendline(b"A"*64 + b"quello che voglio \x01\x02\x03")
r.interactive()

In sequenza:

  • r = process("./dim") crea un sotto-processo eseguendo il programma dim;
  • r.sendline(b"A"*64 + b"quello che voglio \x01\x02\x03") invia la stringa al programma seguita da un newline (\n);
  • r.interactive() ci permette di interagire con il programma come se fosse un terminale.

Non mi rimane che mandare l’indirizzo giusto!

Esercizio

Alcune sfide:

  • Come faccio a trovare il numero di caratteri da scrivere prima dell’indirizzo? (padding)
  • Come faccio a trovare l’indirizzo di una funzione nel caso io non abbia accesso al compilato?
  • Cosa riesco a fare con questa tecnica?
  • Cosa posso fare per evitare che il mio programma sia vulnerabile a questo tipo di attacco?