Category
Web
Chall Author
aparker
Stats
Total Solves: 199/708 Teams
Final Points: 295p
Description
This website will help you understand the rest of the nonsense going on in the CTF. You can even upload your own brainrot words and get definitions!
Preface
The easiest web chall of the CTF and this is one of the ones that took me the longest to figure out.
Looking at the website
The website is composed up by 2 parts: the upload functionality and the dictionary lister:
This reminds me a lot of past file upload vulnerabilities which I used to get RCE...
Source Code Review
Looking at the source code I looked for the logic that handles what we can input. That being the filename and contents of the file:
from flask import Flask, render_template, request, redirect, session, url_for, send_from_directory
import os
import re
import random
import string
from werkzeug.utils import secure_filename
from urllib.parse import unquote
app = Flask(__name__)
app.secret_key = os.urandom(32)
app.config['MAX_CONTENT_LENGTH'] = 1000
# Directory to save uploaded files and images
UPLOAD_FOLDER = 'uploads'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
def create_uploads_dir(d=None):
dirname = os.path.join(UPLOAD_FOLDER, ''.join(random.choices(string.ascii_letters, k=30)))
if d is not None:
dirname = d
session['upload_dir'] = dirname
os.mkdir(dirname)
os.popen(f'cp flag.txt {dirname}')
os.popen(f'cp basedict.brainrot {dirname}')
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
if 'user_file' not in request.files:
return render_template('index.html', error="L + RATIO + YOU FELL OFF")
user_file = request.files['user_file']
if not user_file.filename.endswith('.brainrot'):
return render_template('index.html', error="sorry bruv that aint brainrotted enough")
if 'upload_dir' not in session:
create_uploads_dir()
elif not os.path.isdir(session['upload_dir']):
create_uploads_dir(session['upload_dir'])
fname = unquote(user_file.filename)
if '/' in fname:
return render_template("index.html", error="dont do that")
user_file.save(os.path.join(session['upload_dir'], fname))
return redirect(url_for('dict'))
return render_template('index.html')
@app.route('/dict')
def dict():
if 'upload_dir' not in session:
create_uploads_dir()
elif not os.path.isdir(session['upload_dir']):
create_uploads_dir(session['upload_dir'])
cmd = f"find {session['upload_dir']} -name \\*.brainrot | xargs sort | uniq"
results = os.popen(cmd).read()
return render_template('dict.html', results=results.splitlines())
if __name__ == '__main__':
app.run(debug=False, host="0.0.0.0")
The interesting part here is how the server's backend handles the upload_dir and cmd:
cmd = f"find {session['upload_dir']} -name \\*.brainrot | xargs sort | uniq"
results = os.popen(cmd).read()
return render_template('dict.html', results=results.splitlines())
So xargs basically just fins the .brainrot (our file) and makes an upload dir. My first thought was to try to input cmds like "cat flag.txt" into the end of the filename like:
aaa.brainrot;cat flag.txt
But our extension has to end with .brainrot so that doesn't work.
The Vulnerability
But researching and playing around with xargs locally using the provided source code I found out that xargs splits by spaces!
So we could make file named flag.txt then just space and add basedict.brainrot to get the flag.
Final Payload
flag.txt basedict.brainrot
Flag
UMDCTF{POSIX_no_longer_recommends_that_this_is_possible}
alanoo.dev
My dev page :).
By alanoo 2025-04-30
Table of Contents:
