>
av.Amit VijayanCYBERSECURITY & RESEARCHLet\u2019s connect
Back to the library

Threat Hunting Playbooks

Hunting Server-Side Template Injection: When Your Web Server Starts Spawning Shells

Thumbnail for: Hunting Server-Side Template Injection: When Your Web Server Starts Spawning Shells

Why this matters now

On September 27, 2026, between 3:29 and 4:34 AM, attackers exploited a server-side template injection flaw at Korean online investment firm PFCT. That one vulnerability gave them a path to administrative functions — and from there they exfiltrated customer personal data, including resident registration numbers. Hours later the same day, at 11:54 AM, a second Korean investment firm, Mouda, was hit through website vulnerabilities exploited from multiple IP addresses. Regulators now count 16 financial institutions caught in the cluster, with roughly 100,000 individuals' personal information compromised. Server-side template injection (SSTI) is the current favorite initial-access vector against customer-facing web applications in the region — and it is almost entirely a server-log blind spot for shops that only monitor the endpoint.

How template injection becomes remote code execution

Web frameworks render templates server-side: placeholders like {{ name }} are filled with user-controlled values. When an application passes untrusted input into the template engine itself rather than into a template variable, the attacker controls what gets evaluated. Template engines in common frameworks — Jinja2 and Django templates (Python), Twig (PHP), FreeMarker, Velocity, and Thymeleaf (Java), ERB (Ruby) — each have their own expression syntax: {{ 7*7 }}, ${ 7*7 }, #{ 7*7 }, or <%= 7*7 %>. Attackers probe by submitting these polyglots in form fields, URL parameters, and headers; when a page renders back "49", they have code execution on your server.

From there the playbook is mechanical. MITRE tracks the initial access as T1190 — Exploit Public-Facing Application. The SSTI payload reaches the operating system's shell through the template engine's process — a Java runtime, a Python worker, PHP-FPM, or on IIS the w3wp.exe worker process. The first reliable signal on the endpoint is a web-server process spawning a command shell or a reconnaissance binary. In the PFCT case, the attackers did not stop at a shell: they reached administrative functions and pulled personal data, which means the hunt window starts at the probe and ends at data access.

The KQL hunts

Both queries target Microsoft Defender's DeviceProcessEvents table. Tune the lookback to your retention, and build a short baseline first: on a healthy web server, the worker process should never spawn a shell.

Hunt 1 — Web-server processes spawning shells

This is the highest-fidelity signal for SSTI-turned-RCE. Any web worker spawning cmd.exe, powershell.exe, sh, or bash deserves immediate triage.

DeviceProcessEvents
| where TimeGenerated > ago(7d)
| where InitiatingProcessFileName in~ ("w3wp.exe", "java.exe", "javaw.exe",
    "python.exe", "python3.exe", "node.exe", "php-cgi.exe", "php.exe",
    "httpd.exe", "nginx.exe", "tomcat9.exe", "uwsgi", "gunicorn", "ruby.exe")
| where FileName in~ ("cmd.exe", "powershell.exe", "pwsh.exe", "cscript.exe",
    "wscript.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe",
    "certutil.exe", "bitsadmin.exe", "curl.exe", "wget.exe",
    "sh", "bash", "dash", "perl", "php", "python", "python3")
| project TimeGenerated, DeviceName, InitiatingProcessFileName,
    InitiatingProcessCommandLine, FileName, ProcessCommandLine, AccountName
| order by TimeGenerated desc

Hunt 2 — Enumeration commands from web workers

After the shell comes the recon: attackers enumerate the host before deciding what to steal. These commands are nearly never legitimate when their parent is a web worker.

DeviceProcessEvents
| where TimeGenerated > ago(7d)
| where InitiatingProcessFileName in~ ("w3wp.exe", "java.exe", "javaw.exe",
    "python.exe", "python3.exe", "node.exe", "php-cgi.exe")
    or InitiatingProcessFileName in~ ("cmd.exe", "powershell.exe", "sh", "bash")
    and InitiatingProcessCommandLine has_any ("w3wp", "tomcat", "gunicorn", "uwsgi")
| where FileName in~ ("whoami.exe", "ipconfig.exe", "systeminfo.exe",
    "net.exe", "net1.exe", "nltest.exe", "quser.exe", "qwinsta.exe",
    "hostname", "id", "uname", "ifconfig", "cat", "ls", "ps")
| project TimeGenerated, DeviceName, InitiatingProcessFileName,
    InitiatingProcessCommandLine, FileName, ProcessCommandLine, AccountName
| order by TimeGenerated desc

The Splunk equivalents

Endpoint telemetry catches the shell; web logs catch the probe. SSTI attacks begin with template-syntax strings in requests, and those strings are visible in IIS, Apache, or WAF access logs days before anything executes.

SPL 1 — Template-syntax probes in web access logs

index=web_logs (cs_uri_query="*{{*" OR cs_uri_query="*%7B%7B*" OR
    cs_uri_query="*${*" OR cs_uri_query="*%24%7B*" OR
    cs_uri_query="*#{*" OR cs_uri_query="*%23%7B*" OR
    cs_uri_stem="*{{*" OR cs_uri_stem="*${*")
| stats count, values(c_ip) as src_ips, values(cs_user_agent) as agents
    by cs_uri_stem, cs_uri_query
| sort -count

Cluster the results by source IP and timestamp. A single IP spraying template polyglots across many endpoints is reconnaissance; the same IP returning minutes later with longer payloads is exploitation.

SPL 2 — Process creation with a web-server parent (Windows Security log)

index=wineventlog EventCode=4688
    (ParentProcessName="*w3wp.exe" OR ParentProcessName="*tomcat*"
     OR ParentProcessName="*java.exe")
| stats count by ComputerName, ParentProcessName, NewProcessName, CommandLine
| sort -count

Triage steps

  1. Reconstruct the process tree. From the shell's process ID, walk up: which web worker spawned it, and walk down: what did the shell itself launch? A short-lived whoami followed by silence is different from a shell that spawned a downloader.
  2. Pull the web logs for the same window. Find the requests that carried template syntax — the URI, the parameter, the source IP, the user agent. The probe requests establish initial access; everything after is post-exploitation.
  3. Scope the data access. In the PFCT incident, attackers reached administrative functions and exfiltrated resident registration numbers. Check application audit logs for admin-function calls from the exploited session and database query logs for bulk reads.
  4. Check for persistence and lateral movement. Review scheduled tasks, services, and web-shell drops created by the web worker account in the same window. SSTI rarely ends at one server.
  5. Identify the vulnerable template path. Work with the application team to find which user-controlled input reached the template engine. If you cannot name the injection point, you cannot confirm the fix.

Remediation

  1. Fix the injection point, not just the symptom. Never pass user input to template rendering as code. Use logic-less templates or strict sandboxing for any template that touches untrusted data, and upgrade the framework — vendors ship SSTI hardening in newer releases.
  2. Run the app pool as least privilege. The web worker account should not be able to spawn shells, read credential stores, or reach administrative functions. PFCT's attackers reached admin functions; a properly scoped service account would have contained that step.
  3. Add WAF rules for template syntax. Block {{ }}, ${ }, #{ }, and <% %> patterns in request parameters and headers for applications that have no legitimate use for them — then alert on blocks as reconnaissance.
  4. Rotate what the server could touch. If administrative functions were reachable, treat every credential, API key, and session the application server held as compromised. Reset them before you declare the incident closed.
  5. Audit every template-rendering path. SSTI is a class of bug, not a single endpoint. Review all server-side template rendering across customer-facing applications — the Korean cluster hit 16 institutions because the same weakness class was everywhere.

Keep exploring.

All articles