‹ بازگشت به کتابخانه‌ی رایت‌آپ‌ها
SSTI (Server-Side Template Injection) شدت: بحرانی

اجرای کد از راه دور با SSTI در موتور قالب Jinja2 از طریق فیلد «نام نمایشی»

یک پیام خوش‌آمدگویی که با render_template_string و رشته‌ی خام کاربر ساخته می‌شود، به مهاجم اجازه می‌دهد کد پایتون دلخواه روی سرور اجرا کند.

نویسنده: تیم PentesterHub

فرض کنید یک اپ Flask/Jinja2 (پایتون) می‌خواهد نام کاربر را در یک پیام خوش‌آمدگویی نشان دهد، ولی به‌جای رندر متغیر، کل template را از رشته‌ی ورودی کاربر می‌سازد:

# app.py — نسخه‌ی آسیب‌پذیر
from flask import Flask, request, render_template_string

@app.route('/welcome')
def welcome():
    name = request.args.get('name', 'مهمان')
    template = f'<h1>خوش آمدید، {name}</h1>'
    return render_template_string(template)   # <- کل رشته به‌عنوان TEMPLATE رندر می‌شود!

شناسایی

تست کلاسیک SSTI یک عبارت ریاضی ساده داخل سینتکس قالب است:

GET /welcome?name={{7*7}}

اگر در خروجی به‌جای {{7*7}} عدد 49 دیده شود، یعنی ورودی ما به‌عنوان کد template واقعاً اجرا شده — نه فقط چاپ شده (که نشانه‌ی قطعی SSTI است).

تبدیل به اجرای کد پایتون روی سرور

Jinja2 به اشیاء پایتون دسترسی دارد. با زنجیره‌ای از attributeهای داخلی پایتون (object → subclasses → ...) می‌شود به کلاس‌هایی رسید که اجرای دستور سیستم‌عامل را ممکن می‌کنند:

GET /welcome?name={{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

اگر خروجی چیزی شبیه uid=33(www-data) gid=33(www-data) برگشت، یعنی دستور سیستم‌عامل واقعاً روی سرور اجرا شده — دسترسی کامل اجرای دستور (RCE) از یک پارامتر GET ساده.

چرا این اتفاق می‌افتد

render_template_string دقیقاً همان چیزی را انجام می‌دهد که اسمش می‌گوید: رشته‌ی ورودی را به‌عنوان «کد قالب» کامپایل و اجرا می‌کند. وقتی این رشته حاوی ورودی مستقیم کاربر باشد، عملاً به کاربر اجازه داده‌ایم کد template بنویسد — که در Jinja2 یک زبان Turing-complete با دسترسی به اشیاء پایتون است.

راه‌حل

هیچ‌وقت ورودی کاربر را بخشی از متن قالب نکنید؛ همیشه آن را به‌عنوان یک متغیر context پاس بدهید، نه بخشی از رشته‌ی template:

# نسخه‌ی امن
@app.route('/welcome')
def welcome():
    name = request.args.get('name', 'مهمان')
    return render_template_string('<h1>خوش آمدید، {{ name }}</h1>', name=name)
    # اینجا Jinja2 خودکار مقدار name را escape می‌کند و آن را به‌عنوان کد اجرا نمی‌کند

همین الگو در موتورهای قالب دیگر هم صدق می‌کند (Twig در PHP، ERB در روبی، Blade در لاراول): همیشه داده را از طریق context/متغیر پاس بدهید، نه با ترکیب رشته‌ای قبل از رندر.

تمرین عملی در آزمایشگاه‌های SSTI (Server-Side Template Injection) ›