Temi


Debolezze


  1. Il binario /home/flag02/flag02 stampa un messaggio di debug prolisso. Questo consente all’attaccante di visualizzare il comando risultante dopo un’iniezione. → regola del silenzio
  2. Il binario /home/flag02/flag02 usa indirettamente come input la variabile d’ambiente USER → controllo input
  3. Extra: Il binario /home/flag02/flag02 esegue con privilegi ingiustificatamente elevati → minimo privilegio

🐲 Strategia di attacco


1. Analisi permessi file

All’interno della directory /home/flag02 è presente il binario flag02. Se ne analizzano i permessi.

ls -l /home/flag02

-rwsr-x--- 1 flag02 level02 7438 2011-11-20 21:22 flag02

Si nota che il bit SETUID è attivo e che l’utente owner è proprio flag02. Lo script potrebbe quindi eseguire con i privilegi dell’utente flag02.

2. Si analizzano tutti i possibili canali di output

./flag02

L’output contiene informazioni importanti per l’attacco. In particolare viene mostrato all’utente quale comando verrà eseguito, permettendogli di affinare il suo attacco o dedurre eventuali errori nella sua procedura di attacco, con le informazioni ottenute.

In questo caso in particolare, l’output annuncia che verrà eseguito il comando system("/bin/echo level02 is cool").

Dall’output si nota anche che viene stampato “level02 is cool”. Anche senza analizzare il codice sorgente, si può ipotizzare che il programma utilizzi la variabile d’ambiente che contiene lo username dell’utente attuale, per stampare il messaggio in questione. Analizzando il codice sorgente se ne ha la conferma: getenev("USER").

3. Analisi dei canali di input

Dal codice sorgente si può notare come venga utilizzata la variabile d’ambiente USER come parametro nella costruzione del comando che verrà eseguito successivamente.

È anche possibile dedurlo tracciando l’esecuzione: ltrace ./flag02.

Verifichiamo il valore attuale della variabile d’ambiente USER.

echo $USER

level02

Si può tentare una iniezione di comando nella variabile d’ambiente USER.

4. Sfruttamento della debolezza

Debolezza: Utilizzo in input di una variabile d’ambiente non controllata e modificabile dall’utente, in un file che esegue con permessi elevati e che espone un eccessiva verbosità dell’output.

Per iniettare il comando è sufficiente seguire la logica:

INPUT = INPUT_LEGITTIMO + CARATTERE_SEPARATORE_COMANDI + COMANDO_ARBITRARIO + CARATTERE_CHIUSURA.

In UNIX il carattere separatore è il punto e virgola, mentre il carattere di chiusura è il #.

Quindi si può iniettare il seguente comando:

export USER="level02; getflag #"

Senza il carattere terminatore “#” non è detto che funzioni → i caratteri successivi nella stringa potrebbero essere usati come parametro e generare quindi un errore.

La prima parte “level02” forse si può anche evitare, infatti basta il carattere separatore per evitare che il comando venga interpretato come parametro di echo e quindi stampato, e venga invece eseguito.

A questo punto basterà eseguire flag02 per eseguire getflag con l’utente flag02.

👼 Mitigazione debolezze


Mitigazione #1

Binario flag02 stampa un messaggio di debug prolisso

Basterà eliminare il messaggio di debug eccessivamente verboso, eliminando la seguente riga:

  printf("about to call system(\\"%s\\")\\n", buffer);

L’exploit continuerà ad avere successo, ma non verrà più stampato il messaggio di debug.

Mitigazione #2

Il binario usa indirettamente l’input tramite la variabile d’ambiente USER, tramite il comando asprintf. Tale variabile non è controllata prima dell’esecuzione di system(). Di conseguenza si può iniettare un comando arbitrario in cascata a echo.

Sono possibili due diverse mitigazioni:

  1. Uso di un valore fidato al posto della variabile d’ambiente USER
  2. Sanitizzazione del valore di USER basato su black list
  3. Sanitizzazione del valore di USER basato su white list

A. Uso di un valore fidato al posto di USER

Si crea una variante level2-getpwuid.c del sorgente.

#include <pwd.h>
// [...]

int main(int argc, char **argv, char **envp)
{
	passwd = getpwuid(getuid());
	if (passwd == NULL) {
		perror("getpwuid()");
		exit(EXIT_FAILURE);
	}
	asprintf(&buffer, "/bin/echo %s is cool", passwd->pw_name);
	[...]
}

A me così non funziona, io ho dovuto fare così per farlo funzionare (il controllo == NULL non funziona e l’ho semplicemente tolto):

struct passwd user_passwd = getpwuid(getuid());
asprintf(&buffer, "/bin/echo %s is cool", user_passwd->pw_name);

La struct viene riempita con il record utente in /etc/passwd corrispondente allo uid passato con getuid(), ovvero quello reale. Il campo pw_name della struct contiene lo username.

In questo modo non sarà possibile iniettare alcun comando arbitrario nella variabile USER.

B. Sanitizzazione di USER basata su black list

La sanitizzazione avviene attraverso le due fasi:

  1. Filtro → Si rimuovono tutti i caratteri che rendono possibile l’esecuzione di un comando in BASH
  2. Validazione → Si verifica che lo username risultante non inizi con i caratteri - _

Si crea una variante level2-strpbrk.c del sorgente.

const char invalid_chars[] = "!\\"$&'()*,:;<=>?@[\\\\];

Si settano i caratteri considerati pericolosi.

// asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));
if ((strpbrk(buffer, invalid_chars)) != NULL) {
    perror("strpbrk");
    exit(EXIT_FAILURE);
  }

Si esegue il filtro. La funzione strpbrk ritorna un puntatore al primo byte della stringa corrispondente ad uno dei caratteri invalidi. Se esso è non nullo, significa che è presente almeno un carattere invalido e quindi si esce con un messaggio di errore.

if (getenv("USER")[0] == '-' || getenv("USER")[0] == '_') {
    printf("Invalid username\\n");
    exit(EXIT_FAILURE);
  }

Si esegue poi la validazione. Se il primo carattere è - o _ viene stampato un messaggio di errore e si esce.

Anche in questo caso, non sarà più possibile iniettare un comando arbitrario nella variabile USER.

C. Sanitizzazione di USER basata su white list

Questo tipo di sanitizzazione è più potente. Si considera valido uno username se contiene solo e soltanto i seguenti caratteri: 0-9, a-z, A-Z, -, _

Si crea una variante level2-isalnum.c del sorgente.

#include <ctype.h>  // per la funzione isalnum()
****
int main(int argc, char **argv, char **envp)
{
  char *buffer, *p;
  // [...]
  // asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));
  p = getenv("USER");
  
  while (*p != 0) {
    if (isalnum(*p) || *p == '-' || *p == '_')
      p++;
    else {
      printf("Invalid username.\\n");
      exit(EXIT_FAILURE);
    }
  }
}

Si fa puntare p al valore della variabile d’ambiente USER. Si cicla poi su p fino alla terminazione della stringa. Se il caratter puntato da p è valido si passa al successivo, altrimenti si esce con un errore. Anche in questo caso si rende impossibile l’iniezione di qualunque codice arbitrario nella variabile USER.

Mitigazione #3

Non presente sulle slide.

Come per gli altri casi di esecuzione privilegi elevati si può:

  1. Rimuovere bit SETUID se non necessario
  2. Privilege drop quando non necessari

Questa mitigazione rende l’attacco inefficace