HackTheBox Challenge - Spookifier
Challenge
This is a small, beginner-friendly HackTheBox challenge that demonstrates an uncommon web vulnerability. It’s simple but instructive—great for learning how to identify and exploit subtle issues in web applications. Let’s dive in and see what makes it interesting.
Downloading the files
The challenge provides the application’s source files so we can inspect the code locally. I’ll use these files to review the implementation and run the web app in a Docker container to reproduce the behavior and test potential vulnerabilities. After downloading the files, we can see the following structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── challenge
│ ├── application
│ │ ├──blueprints
│ │ ├──static
│ │ ├──templates
│ │ ├──main.py
│ │ └──utils.py
├── config
│ ├── supervisord.conf
├── build-docker.sh
├── Dockerfile
└── flag.txt
The main application code is in the challenge/application directory. That’s where we’ll focus our attention. The first file to look at is main.py, which is the entry point of the application.
Reviewing the code
Opening main.py, we can see that it’s a Flask application. The code is relatively straightforward, setting up the app, registering blueprints, and defining error handlers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import Flask, jsonify
from application.blueprints.routes import web
from flask_mako import MakoTemplates
app = Flask(__name__)
MakoTemplates(app)
def response(message):
return jsonify({'message': message})
app.register_blueprint(web, url_prefix='/')
@app.errorhandler(404)
def not_found(error):
return response('404 Not Found'), 404
@app.errorhandler(403)
def forbidden(error):
return response('403 Forbidden'), 403
@app.errorhandler(400)
def bad_request(error):
return response('400 Bad Request'), 400
From this, we can see that the application uses Mako templates for rendering HTML. The main functionality is likely in the blueprints/routes.py file, so let’s check that next.
In routes.py, we find the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Blueprint, request
from flask_mako import render_template
from application.util import spookify
web = Blueprint('web', __name__)
@web.route('/')
def index():
text = request.args.get('text')
if(text):
converted = spookify(text)
return render_template('index.html',output=converted)
return render_template('index.html',output='')
The index route takes a text parameter from the query string, processes it with the spookify function from utils.py, and then renders the index.html template with the converted text.
The spookify function is where the main logic resides. Let’s look at utils.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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from mako.template import Template
font1 = {
'A': '𝕬',
'B': '𝕭',
...
}
font2 = {
'A': 'ᗩ',
'B': 'ᗷ',
'C': 'ᑢ',
...
}
font3 = {
'A': '₳',
'B': '฿',
'C': '₵',
...
}
font4 = {
'A': 'A',
'B': 'B',
'C': 'C',
...
}
def generate_render(converted_fonts):
result = '''
<tr>
<td>{0}</td>
</tr>
<tr>
<td>{1}</td>
</tr>
<tr>
<td>{2}</td>
</tr>
<tr>
<td>{3}</td>
</tr>
'''.format(*converted_fonts)
return Template(result).render()
def change_font(text_list):
text_list = [*text_list]
current_font = []
all_fonts = []
add_font_to_list = lambda text,font_type : (
[current_font.append(globals()[font_type].get(i, ' ')) for i in text], all_fonts.append(''.join(current_font)), current_font.clear()
) and None
add_font_to_list(text_list, 'font1')
add_font_to_list(text_list, 'font2')
add_font_to_list(text_list, 'font3')
add_font_to_list(text_list, 'font4')
return all_fonts
def spookify(text):
converted_fonts = change_font(text_list=text)
return generate_render(converted_fonts=converted_fonts)
The spookify function converts the input text into four different “spooky” fonts using predefined mappings in font1, font2, font3, and font4. It then generates an HTML table with the converted text.
First let’s see how the application behaves when we input some text. Running the application in a Docker container, we can access it in our browser.
The application takes the input text and displays it in four different spooky fonts. We need to find a way to exploit this functionality, maybe through an injection vulnerability in the text input, but how exactly?
Finding the vulnerability
The key to finding the vulnerability lies in the use of Mako templates. Mako allows for dynamic content rendering, which can be dangerous if user input is not properly sanitized. If we can inject Mako template syntax into the text parameter, we might be able to execute arbitrary code on the server.
To test this, we can try injecting a simple Mako expression into the text parameter. For example, we can use ${7*7} to see if it evaluates to 49.
Great ! The expression was evaluated, confirming that we can execute arbitrary Mako code. But before going further, let me explain how Server-Side Template Injection (SSTI) works.
What is SSTI?
Server-Side Template Injection (SSTI) is a web security vulnerability that occurs when user input is not properly sanitized and is directly included in server-side templates. This can allow an attacker to inject and execute arbitrary code on the server, potentially leading to data leakage, remote code execution, or other malicious activities.
How template engines work: Template engines are used to generate dynamic HTML content by combining static templates with dynamic data. They often support expressions, control structures, and functions that can be executed on the server side.
In the case of the challenge the portion of the code that is vulnerable to SSTI is the generate_render function in utils.py.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def generate_render(converted_fonts):
result = '''
<tr>
<td>{0}</td>
</tr>
<tr>
<td>{1}</td>
</tr>
<tr>
<td>{2}</td>
</tr>
<tr>
<td>{3}</td>
</tr>
'''.format(*converted_fonts)
return Template(result).render()
The result variable is a string that contains HTML code with placeholders for the converted fonts. The Template(result).render() line creates a Mako template from the result string and renders it. Since the converted_fonts list contains user input, if an attacker can inject Mako syntax into the text parameter, it will be included in the result string and executed when the template is rendered.
Exploiting the vulnerability
Now that we know we can execute arbitrary Mako code, we can try to read the contents of the flag.txt file. In Python, we can use the open function to read files. In order to leverage this, we can use the following payload style:
${payload}
Why this style ?
- The
${}syntax is used in Mako templates to evaluate expressions. By wrapping our payload in${}, we ensure that it gets evaluated by the Mako engine.
Our goals is to read the flag.txt file, which is located in the root directory of the application. To read the file, we can use the following payload: ${import(‘os’).popen(‘cat ../flag.txt’).read()}
This payload uses the __import__ function to import the os module, then uses os.popen to execute the cat ../flag.txt command and read its output.
How to protect against SSTI
To protect against SSTI vulnerabilities, developers should follow these best practices:
- Input Validation: Always validate and sanitize user input to ensure it does not contain any malicious content.
- Use Safe Template Engines: Choose template engines that have built-in protections against code injection.
- Escape User Input: Ensure that any user input included in templates is properly escaped to prevent it from being interpreted as code.
- Least Privilege: Run the application with the least privileges necessary to limit the impact of a potential exploit.
- Regular Security Audits: Regularly review and audit the codebase for potential vulnerabilities.
Here’s how to patch the code in utils.py to prevent SSTI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def generate_render(converted_fonts):
result = '''
<tr>
<td>{0}</td>
</tr>
<tr>
<td>{1}</td>
</tr>
<tr>
<td>{2}</td>
</tr>
<tr>
<td>{3}</td>
</tr>
'''.format(*converted_fonts)
# Escape user input to prevent SSTI
safe_result = result.replace('${', '${').replace('<%', '<%').replace('%>', '%>')
return Template(safe_result).render()
Conclusion
This challenge was a great exercise in identifying and exploiting Server-Side Template Injection (SSTI) vulnerabilities. It highlights the importance of properly sanitizing user input and being cautious when using template engines that allow for dynamic code execution. Always validate and sanitize any user input before including it in templates to prevent such vulnerabilities.




