Server-Side Template Injection: Từ nhúng biến đến RCE
Cơ chế template engine render dữ liệu động, kỹ thuật nhận diện engine (Jinja2, Thymeleaf, Freemarker) và khai thác gadget chain thực thi mã.
TL;DR: Server-Side Template Injection (SSTI) xảy ra khi ứng dụng cho phép input của người dùng trực tiếp nhúng vào cú pháp template thay vì chỉ truyền dưới dạng context variable. Khác với XSS vốn chỉ thực thi trên trình duyệt người dùng, SSTI chạy ngay tại máy chủ web, mở ra con đường xâm nhập cực kỳ nguy hiểm. Bằng cách phân tích cây cú pháp của engine (Jinja2, Mako, Thymeleaf, Freemarker) và lần theo các thuộc tính gốc (__class__, __mro__, __subclasses__), kẻ tấn công có thể thoát khỏi sandbox và kích hoạt Remote Code Execution (RCE).
1. Scenario — Tính năng tùy biến email chúc mừng và vỏ shell đảo ngược
Một nền tảng SaaS tiếp thị tự động cho phép người dùng tùy biến mẫu email gửi tới khách hàng. Trong giao diện biên soạn, ứng dụng cung cấp tính năng xem trước (Preview) với lời chào cá nhân hóa:
# Vulnerable email preview controller (Python Flask + Jinja2)
from flask import Flask, request, render_template_string
@app.route('/api/email/preview', methods=['POST'])
def email_preview():
user_template = request.form.get('template', 'Xin chào {{ name }}')
# Bug: Directly rendering user-controlled string as Jinja2 template
rendered_output = render_template_string(user_template, name="Nguyen Van A")
return {"html": rendered_output}
Lập trình viên nghĩ rằng tính năng này hoàn toàn vô hại vì Jinja2 mặc định đã bật tính năng tự động escape HTML (autoescape=True), giúp ngăn chặn triệt để tấn công Cross-Site Scripting (XSS).
Tuy nhiên, khi một nhà nghiên cứu an ninh nhập vào ô template biểu thức:
{{ 7 * 7 }}
Email xem trước trả về con số 49. Máy chủ không chỉ in dữ liệu mà đang trực tiếp thông dịch và thực thi biểu thức toán học. Tiếp tục mở rộng biểu thức, chuyên gia khai thác chuỗi đối tượng Python nội bộ và mở thành công một Reverse Shell điều khiển toàn bộ cụm máy chủ container của doanh nghiệp.
2. Analogy — Khuôn đúc bánh quy và máy in 3D công nghiệp
Hãy so sánh cách hoạt động của hai loại công cụ tạo hình:
- Khuôn đúc bánh quy tĩnh (Safe template rendering): Khuôn kim loại có sẵn hình ngôi sao cố định. Bạn đổ bột mì (dữ liệu người dùng) vào khuôn. Bột chỉ lấp đầy khoảng trống đã được định hình sẵn. Bột không thể làm biến dạng cấu trúc của khuôn thép.
- Máy in 3D công nghiệp (SSTI vulnerability): Bạn gửi trực tiếp bản thiết kế mã G-code (template string) cho máy in thực thi. Nếu người dùng không gửi bản vẽ bánh quy mà gửi mã lệnh điều khiển đầu mũi khoan đâm xuyên bảng mạch điều khiển, máy in sẽ tự phá hủy chính nó.
Trong SSTI, việc cho phép người dùng sửa đổi trực tiếp file template tương đương với việc trao quyền nạp mã G-code cho máy in 3D:
| Thực thể đời sống | Cơ chế trong Server-Side Template Injection |
|---|---|
| Khuôn đúc định hình | File template tĩnh được biên dịch trước (welcome.html) |
| Bột mì đổ vào khuôn | Dữ liệu ngữ cảnh (context data: name="Van A") |
| Bản vẽ mã G-code độc hại | Payload cú pháp template: {{ 7*7 }} hoặc ${7*7} |
| Mũi khoan phá hủy máy | Chuỗi gadget leo thang gọi os.popen() hoặc Runtime.getRuntime() |
3. Vì sao template engine biến dữ liệu nhập thành lệnh thực thi?

Các template engine hiện đại như Jinja2 (Python), Twig (PHP), Freemarker (Java) hay ERB (Ruby) được thiết kế rất mạnh mẽ. Chúng không đơn thuần là công cụ thay thế chuỗi (Search & Replace) mà là các ngôn ngữ thông dịch thu nhỏ với đầy đủ khả năng tính toán, rẽ nhánh điều kiện và gọi phương thức đối tượng.
Trong bài học trước, bạn đã thấy việc phân tách giữa lệnh và dữ liệu quan trọng như thế nào:
Ôn lại nguyên lý phân tách giữa mã lệnh điều khiển và dữ liệu người dùng trong bộ phân tích cú pháp.
Trong template engine, khi lập trình viên dùng render_template_string(user_input), toàn bộ chuỗi của người dùng được đưa vào giai đoạn biên dịch cây cú pháp trừu tượng (Abstract Syntax Tree - AST). Các ký tự đặc biệt như {{ ... }}, {% ... %}, ${ ... } được bộ phân tích từ vựng (Lexer) nhận diện là khối lệnh điều khiển (Control Block).
Cây quyết định nhận diện Template Engine (Detection Tree)
Để khai thác thành công, kẻ tấn công phải xác định chính xác backend đang chạy engine nào thông qua các payload trắc nghiệm:
- Gửi payload:
${7*7}- Nếu trả về
49: Khả năng cao là template engine thuộc họ Java (Freemarker, Velocity) hoặc Smarty. - Nếu trả về nguyên văn
${7*7}: Chuyển sang bước 2.
- Nếu trả về
- Gửi payload:
{{7*7}}- Nếu trả về
49: Khả năng là Jinja2, Twig hoặc Mako. - Nếu trả về lỗi cú pháp: Không hỗ trợ cú pháp
{{ }}.
- Nếu trả về
- Phân biệt Jinja2 vs Twig bằng phép nhân chuỗi:
{{ 7 * '7' }}- Jinja2 (Python): Nhân chuỗi tạo thành
7777777(chuỗi gồm 7 số bảy). - Twig (PHP): Ép kiểu chuỗi thành số và nhân toán học, trả về
49.
- Jinja2 (Python): Nhân chuỗi tạo thành
4. Kỹ thuật đào Gadget Chain leo thang RCE trên Python Jinja2
Khi đã xác định ứng dụng dùng Jinja2, mục tiêu tiếp theo là tìm kiếm một chuỗi phương thức dẫn tới lệnh thực thi hệ điều hành (subprocess hoặc os.system).
Python quản lý mọi thứ dưới dạng đối tượng (Object), và mỗi đối tượng đều duy trì tham chiếu ngược về lớp cha và không gian tên gốc:
- Tìm một đối tượng sống bất kỳ: Ví dụ chuỗi rỗng
"". - Truy cập class của đối tượng:
"".__class__trả về<class 'str'>. - Leo lên lớp gốc (Root Object):
"".__class__.__mro__hoặc"".__class__.__base__trả về<class 'object'>. - Duyệt danh sách các lớp con trong bộ nhớ: Gọi
object.__subclasses__(). Hàm này trả về một danh sách (mảng) gồm hàng trăm lớp đang được nạp trong tiến trình Python hiện tại.
# Payload inspect subclasses in Jinja2
{{ "".__class__.__mro__[1].__subclasses__() }}
Trong danh sách hàng trăm class con này, ta tìm kiếm các class có khả năng mở tiến trình hệ thống, ví dụ:
subprocess.Popenos._wrap_closewarnings.catch_warnings
Giả sử subprocess.Popen nằm ở vị trí chỉ số 407 trong mảng subclasses, payload RCE hoàn chỉnh sẽ là:
{{ "".__class__.__mro__[1].__subclasses__()[407]('cat /etc/passwd', shell=True, stdout=-1).communicate()[0] }}
Lệnh trên trực tiếp khởi tạo một tiến trình con, thực thi câu lệnh đọc file /etc/passwd và in kết quả ra màn hình trình duyệt của kẻ tấn công.
# Notification message formatter
from jinja2 import Environment
def format_alert(user_name, alert_text):
# Bug: Formatting user string into template definition
raw_tpl = f"Hello {user_name}, alert notice: " + alert_text
env = Environment()
template = env.from_string(raw_tpl)
return template.render()Thử ngẫmVì sao trong môi trường container Docker đã chạy quyền non-root, lỗ hổng RCE qua SSTI vẫn cực kỳ nguy hiểm?
Bởi vì kẻ tấn công có thể đọc trộm các biến môi trường (Environment Variables) chứa database credentials, API secret keys, hoặc pivoting vào mạng nội bộ của cụm Kubernetes.
5. Pitfalls & Giải pháp khắc phục triệt để
❌ Bẫy thường gặp: Lọc từ khóa hoặc trông cậy vào Sandbox nửa vời
Nhiều lập trình viên cố gắng lọc các từ khóa như class, mro, subclasses hoặc dấu gạch dưới __:
# ANTI-PATTERN: Weak keyword blacklist
if "__" in template_str or "class" in template_str:
abort(400, "Payload khong hop le")
Kẻ tấn công dễ dàng vượt qua bộ lọc bằng cách tận dụng hàm lấy thuộc tính gián tiếp attr() hoặc mã hóa hex/char:
<!-- Bypass blacklist through request args or dict lookup -->
{{ ()|attr(request.args.c)|attr(request.args.m) }}
<!-- Or using hex characters in strings -->
{{ ()['\x5f\x5fclass\x5f\x5f'] }}
Jinja2 từng cung cấp SandboxedEnvironment, nhưng lịch sử bảo mật cho thấy sandbox của Jinja2 liên tục bị bypass qua các gadget mới và không được khuyến nghị cho dữ liệu người dùng không tin cậy.
✅ Giải pháp chuẩn mực: Phân tách Template tĩnh và Context Data
Cách khắc phục triệt để duy nhất là tuyệt đối không bao giờ cho phép người dùng chỉnh sửa hoặc ghép chuỗi vào cấu trúc template. Mọi file template phải được lưu cố định trên ổ đĩa và chỉ nhận dữ liệu thông qua từ điển ngữ cảnh (Context Dictionary):
# SECURE PATTERN: Static template file with isolated context parameters
from flask import render_template
@app.route('/api/email/preview', methods=['POST'])
def email_preview():
# Only allow predefined template choices, never raw template strings
template_type = request.form.get('template_type', 'standard')
allowed_templates = {
'standard': 'emails/standard_notice.html',
'promo': 'emails/promo_notice.html'
}
target_file = allowed_templates.get(template_type, 'emails/standard_notice.html')
# Safe: Input is treated strictly as data, never parsed as template AST
return render_template(
target_file,
user_name=request.form.get('user_name', ''),
message=request.form.get('message', '')
)
Nếu bài toán nghiệp vụ bắt buộc phải cho phép người dùng tùy biến giao diện linh hoạt, hãy sử dụng các thư viện template không có logic (Logicless Templates) như Mustache hoặc Handlebars. Các thư viện này chỉ hỗ trợ phép thế biến đơn giản và hoàn toàn không cho phép gọi phương thức hay leo thang đối tượng.
6. Đào sâu
Tài liệu nghiên cứu và danh mục gadget chain chuẩn:
- James Kettle — Server-Side Template Injection: RCE in your Flat-pack City: Công trình nghiên cứu nguyên bản năm 2015 đặt nền móng cho kỹ thuật phát hiện và khai thác SSTI.
- PayloadsAllTheThings — Server Side Template Injection: Bộ sưu tập gadget chain RCE thực chiến cho hơn 15 template engine phổ biến.
- OWASP Testing Guide — Testing for Server-Side Template Injection (WSTG-INPV-18): Quy trình kiểm thử xâm nhập tiêu chuẩn quốc tế.
7. Tóm tắt
- Server-Side Template Injection xảy ra khi template engine nhận chuỗi do người dùng kiểm soát và biên dịch thành cây cú pháp lệnh thay vì dữ liệu thuần.
- Khác với XSS chỉ gây hại trên trình duyệt khách, SSTI thực thi trực tiếp tại máy chủ và thường dẫn tới Remote Code Execution (RCE).
- Cây quyết định SSTI sử dụng các biểu thức toán học đặc thù (
{{7*7}},${7*7},{{7*'7'}}) để nhận diện chính xác backend engine. - Trên Jinja2, kẻ tấn công leo theo chuỗi đối tượng gốc (
__class__.__mro__[1].__subclasses__()) để tìm các lớp nạp tiến trình nhưsubprocess.Popen. - Khắc phục triệt để bằng cách cố định file template tĩnh, nạp dữ liệu qua context dictionary hoặc sử dụng logicless templates như Mustache.
8. Tự kiểm tra
- Q1Phân biệt sự khác nhau cơ bản giữa lỗ hổng Cross-Site Scripting (XSS) và Server-Side Template Injection (SSTI)?
- Q2Tại sao tính năng tự động escape HTML (autoescape=True) trong Jinja2 hoàn toàn vô dụng trước tấn công SSTI?
- Q3Giải thích ý nghĩa của thuộc tính __mro__ trong chuỗi gadget Python và vai trò của nó khi khai thác SSTI?
- Q4Nếu ứng dụng chạy trên ngôn ngữ Java sử dụng Thymeleaf hoặc Freemarker, cơ chế khai thác SSTI có gì khác so với Python Jinja2?
- Q5Tại sao việc sử dụng Logicless Template như Mustache lại có thể loại bỏ hoàn toàn nguy cơ SSTI RCE?
Bài tiếp theo: OS Command Injection và nghệ thuật Bypass bộ lọc Shell
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Second-order SQL Injection: Khi dữ liệu kích hoạt ở bước hai
Bài tiếpOS Command Injection và nghệ thuật Bypass bộ lọc Shell
Thảo luận & góp ý
Hỏi đáp về bài này