ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

HTTP Host头攻击漏洞:从扫描器告警到修复复测全流程

HTTP Host头攻击漏洞:从扫描器告警到修复复测全流程 1. 扫描器甩来一顶帽子先把HTTP Host头攻击漏洞说清楚早上打开漏洞管理平台看到一条高危标注检测到目标URL存在HTTP Host头攻击漏洞。点进去看详情请求包里就是改了一个 Host 头响应里居然把那个伪造的域名原样吐了回来。这种场景我在做企业资产自查和修复跟进的时候遇到过太多次有的确实是真漏洞有的纯粹是扫描器宁可错杀的结果。这篇文章想干的事情很直接把 Host 头攻击这条漏洞从原理、利用路径、手工验证、修复落地到复测回扫整条链路拆开讲一遍。内容适合三类人看——刚接手安全运维、需要给出修复方案的开发同学在做资产管理、被扫描器报告淹了的运维同学以及想搞清楚为什么改个请求头就能算高危的初级安全从业者。全文的技术判断都基于常见工程实践涉及配置和代码的部分我会给到可直接抄改的片段但请务必只在你自己拥有或已获得书面授权的资产上做验证。先把结论摆在前面Host 头本身不是漏洞信任 Host 头才是。HTTP/1.1 规定客户端必须带 Host 头但它同时规定这个头由客户端自己填。服务端如果把它当成可信输入去拼接链接、做路由、算缓存键攻击面就打开了。反过来只要在入口层和应用层各设一道校验这类问题的修复成本其实很低。2. 一个小小请求头凭什么变成攻击入口2.1 HTTP/1.1 里 Host 头的特殊地位早年 HTTP/1.0 时代一个 IP 对应一个站点请求里不带域名服务器也不需要知道你要访问谁。后来共享主机和虚拟主机普及同一台机器、同一个端口上跑几十个站点服务器必须靠一个字段来判断这个请求到底属于哪个站。这个字段就是 HostHTTP/1.1 里把它从可选变成了必填。问题就出在这个必填上。它是请求头是客户端完全可控的输入。你写Host: www.example.com服务器认你写Host: evil.test服务器照样收到。服务器要做的判断是这个值我该不该信信到什么程度很多框架和中间件在帮你自动处理这件事的时候默认是信的。比如生成绝对 URL、拼重定向地址、拼邮件正文里的链接用的都是当前请求的 Host。我一般喜欢用一个类比跟开发同学解释Host 头相当于访客自己写在门口的纸条上面写着我找张三。门卫如果直接按纸条把包裹寄到纸条上写的地址那这张纸条就是攻击面。正确的做法是门卫自己有一份住户名单名单上有的才放行没有的一律退回。2.2 扫描器到底测了什么才报出来绝大多数扫描器对这个漏洞的检测逻辑非常朴素发一个正常请求再发一个把 Host 改成随机域名的请求比较两次响应。如果第二次依然返回 200、页面长度相似、而且响应体或响应头里出现了那个随机域名就判定存在 Host 头攻击。这里有两个容易误判的地方实践中必须人工复核。第一种误判服务端对所有 Host 都返回同一个默认站点。这是单站点部署下非常常见的现象Nginx 没有配default_server或者只配了一个server_name但没做兜底于是任何 Host 都能命中这个唯一的虚拟主机。响应 200 是真的但页面里的链接依然是写死的正式域名这不构成可利用漏洞属于配置健壮性问题。第二种误判扫描器只改了Host但没测X-Forwarded-Host、X-Forwarded-Server、X-Host、X-Forwarded-For这几个变体。有些应用在反向代理后面框架只信任转发头而不信任原始 Host扫描器换个头就漏了。反过来也有扫描器把这些头全打一遍结果误报出一堆。所以拿到这条报告的第一反应不该是赶紧改而是先确认它到底把哪个值信了、用在哪了。判断依据我后面会给一套具体流程。2.3 真正被利用时攻击者想要的是什么理解利用价值才能判断危害等级。Host 头被利用攻击者通常图三样东西。一是拿到凭据。密码重置、邀请链接、邮箱验证这类功能如果按请求 Host 拼链接攻击者触发一次重置受害者收到的邮件里链接指向攻击者域名token 就跟着泄露了。这条链路在真实事件里出现过很多次危害等级高。二是绕过访问控制。部分应用用 Host 做内部路由判断比如Host: internal.admin.local才能访问管理后台或者靠 Host 决定走哪个后端上游。攻击者伪造这个头可能直接摸到不该暴露的接口。三是污染缓存或日志。CDN、Nginx 缓存层如果缓存键里含 Host攻击者可以先用恶意 Host 预热一份内容等正常用户来访问时命中被污染的缓存。日志污染则是把攻击者域名写进安全审计日志干扰事后溯源。3. 五条常见利用路径逐条对号入座3.1 密码重置链接投毒这是 Host 头攻击里最经典、也最容易造成实际损失的一条。流程很简单用户提交邮箱应用生成一个带 token 的重置链接发到用户邮箱。如果链接的域名部分是从request.host取的攻击者只要在提交时把 Host 改成自己的域名受害者邮箱里收到的就是https://attacker.test/reset?tokenxxx。我见过一份真实报告攻击者甚至不需要提前控制一个域名只要用自己注册的相似域名配合证书普通用户很难分辨。用户点进去token 就进了攻击者服务器接着攻击者拿这个 token 去正站改密码。判断这条路径是否成立有两个观察点重置邮件正文里的链接域名以及提交重置请求时 Host 头被替换后服务器的响应行为。只要邮件链接跟着变了基本可以定性。正确的写法是把基础域名做成配置项。比如 Node 里// 不要这样写 const link ${req.protocol}://${req.headers.host}/reset?token${token}; // 应该这样写 const BASE_URL process.env.PUBLIC_BASE_URL; // https://www.example.com if (!BASE_URL) throw new Error(PUBLIC_BASE_URL is required); const link ${BASE_URL}/reset?token${token};Django 里对应的是ALLOWED_HOSTS配合request.build_absolute_uri的显式域名覆盖。Rails 里是config.hosts加default_url_options[:host]。核心思路完全一致链接域名必须来自配置不来自请求。3.2 重定向与缓存劫持登录成功后跳转、短链跳转、多语言站点按域名跳转这些功能都爱用 Host 拼 Location 头。攻击者改 Host 发请求拿到Location: https://evil.test/login然后把这条链接发给别人或者配合开放重定向做钓鱼跳板。缓存层面更隐蔽。假设 CDN 的缓存键是域名 路径攻击者用伪造 Host 访问/homeCDN 会把响应缓存到evil.test:/home。正常用户访问www.example.com/home不会命中这份缓存所以这条路径单独看危害有限。真正出问题的是缓存键设计不当比如 CDN 只按路径缓存、忽略 Host或者应用层自己做了页面缓存且键里不含 Host那攻击者预热的内容就会被所有用户看到。排查缓存问题有个土办法连续发两种不同 Host 的请求看回源和缓存命中情况。如果两次响应完全一致且第二次明显变快说明缓存键可能没区分域名得进一步看缓存层配置。3.3 虚拟主机路由绕过有些内网系统把管理后台和对外站点部署在同一台服务器上靠server_name或应用层的 Host 判断来区分。入口层如果没有默认站点兜底攻击者把手里的域名解析指向这台服务器再带Host: admin.internal请求就可能直达管理界面。Tomcat 的RemoteIpValve、Spring Boot 的server.forward-headers-strategy、Nginx 的proxy_set_header Host $host这些配置如果写得不对等于主动把内部主机名暴露给外部请求。我处理过一起案例应用层做了 Host 白名单但白名单里包含了localhost和127.0.0.1方便本地调试结果线上仍然生效。这类调试后门是排查时的重点。这类问题的修复不复杂核心是入口层必须有一个明确的默认虚拟主机对未知 Host 直接拒绝而不是让它落到某个真实站点上。3.4 转发头带来的连带风险真实生产环境里请求往往要经过 CDN、负载均衡、Nginx 好几层。每层都会往请求里加X-Forwarded-For、X-Forwarded-Host、X-Forwarded-Proto等头记录原始信息。应用框架读了这些头用它们还原用户看到的域名和协议。风险在于如果入口层没有清洗这些头攻击者可以自己伪造。比如直接带X-Forwarded-Host: evil.test打过来Nginx 原样透传应用框架读到后当成真实域名用。我在做资产排查时一定会把X-Forwarded-Host和Host两个都测一遍因为这两条路的修复位置不同。Tomcat 的RemoteIpValve配置里有个internalProxies参数用来声明哪些上游地址是可信的只有来自这些地址的转发头才被采纳。这个参数如果不配或配得过宽伪造头就能生效Valve classNameorg.apache.catalina.valves.RemoteIpValve remoteIpHeaderX-Forwarded-For protocolHeaderX-Forwarded-Proto internalProxies10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}/3.5 日志、监控与邮件头污染这条路径危害相对低但在合规审计里会被点名。攻击者用伪造 Host 大量请求把恶意域名灌进访问日志、错误日志、告警系统。事后做事件溯源时日志里全是垃圾数据干扰判断。还有一类是邮件头注入的变体。某些应用把 Host 值拼进邮件头或者邮件正文的发送者信息里攻击者构造带特殊字符的 Host 值可能导致邮件被判定为异常或进入垃圾箱。这类问题复现成本高一点但排查时值得一并看。4. 手工验证一套可以照着做的检测流程4.1 授权边界与测试环境准备检测之前先确认一件事你测的资产是不是你的或者你有没有书面授权。企业内部自查、SRC 平台已收录的目标、自己搭的靶场这三类可以做。其他情况不要碰改 Host 打别人的站在很多地方算未授权访问风险很大。测试环境我一般这么搭一台装了 Nginx 的测试机上面跑一个简单的应用故意留一个用 Host 拼链接的密码重置接口。这样修复前后都能对比验证不用拿生产环境冒险。本地起服务时记得让测试域名能解析到本机可以改/etc/hosts# 在测试机上执行仅用于本地环境 echo 127.0.0.1 www.demo.local | sudo tee -a /etc/hosts要抓服务器拿我的域名发了请求这种证据可以用 DNSLog 类平台。你在 Host 头里填一个自动生成的子域名如果服务器真的拿这个值去做了解析或拼了链接DNSLog 平台上就能看到记录。这个手法在验证缓存投毒和重置链接投毒时特别有效但同样只用于自己的资产。4.2 用 curl 做第一轮探测工具不用复杂curl 足够。第一轮先看响应状态和响应体里有没有回显# 基线请求确认正常行为 curl -s -o /dev/null -w %{http_code} %{redirect_url}\n https://www.example.com/ # 替换 Host看状态码和跳转目标 curl -s -o /dev/null -w %{http_code} %{redirect_url}\n \ -H Host: evil.test https://www.example.com/ # 看响应体里是否回显了伪造域名 curl -s -H Host: evil.test https://www.example.com/ | grep -i evil.test # 测试转发头变体 curl -s -o /dev/null -w %{http_code} %{redirect_url}\n \ -H X-Forwarded-Host: evil.test https://www.example.com/ # 测试内部主机名是否可达 curl -s -o /dev/null -w %{http_code}\n \ -H Host: localhost https://www.example.com/看结果的时候按这个逻辑走返回 301 且 Location 指向固定的正式域名说明做了硬编码基本安全返回 200 且响应体或 Location 里出现evil.test属于高概率可利用返回 400 或连接直接断开说明入口层已经做了校验是好事。4.3 判定能不能打的三个关键观察点第一回显位置。是出现在 HTML 正文的link relcanonical、og:url、表单 action 里还是只出现在响应头的Location出现在绝对 URL 里的危害明显更高因为浏览器会真的往那个地址发请求。第二是否持久化。如果这个恶意值被写进了数据库、缓存、日志那么影响就不是单次请求了。我一般会改动 Host 提交一次然后用正常 Host 再访问同一路径看恶意值是否还在。还在说明持久化了危害升级。第三是否有凭据流转。如果回显位置在重置邮件、邀请链接、支付回调地址里直接按高危处理。这类功能我会单独测一遍邮件正文光看页面回显不够。4.4 从单点探测到批量自查资产多的时候手工打不现实。我一般写一个脚本做首轮筛只判断换 Host 后响应是否回显伪造域名这一个条件命中的再人工复核。Python 版本大概这样import requests TARGETS [https://www.example.com/, https://api.example.com/] def probe(url, injected_host): headers {Host: injected_host} try: r requests.get(url, headersheaders, timeout8, allow_redirectsFalse) except requests.RequestException as e: return url, error, str(e) hit injected_host in r.text or injected_host in r.headers.get(Location, ) return url, r.status_code, HIT if hit else clean if __name__ __main__: for t in TARGETS: print(probe(t, probe.invalid.example))注意脚本里换 Host 之后证书校验会出问题测试环境里可以临时关掉生产自查建议只对已备案的域名做避免触发风控。这批结果拿回来重点看状态码 200 加 HIT 的那些。5. 修复落地入口层到应用层的四道闸门5.1 入口层先兜底未知 Host 直接拒Nginx 的修法是把默认站点和真实站点拆开。默认站点不接受任何业务直接断连或者返回 444。这段配置我在多个项目里都用过效果稳定# 默认站点兜住所有未知 Host server { listen 80 default_server; listen 443 ssl default_server; server_name _; ssl_certificate /etc/nginx/certs/default.crt; ssl_certificate_key /etc/nginx/certs/default.key; return 444; } # 真实站点只接受明确列出的域名 server { listen 80; listen 443 ssl; server_name www.example.com example.com; ssl_certificate /etc/nginx/certs/example.crt; ssl_certificate_key /etc/nginx/certs/example.key; location / { proxy_pass http://app_backend; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; } }return 444是 Nginx 特有的写法直接关闭连接不回任何响应。相比返回 403它对扫描器更沉默也能减少无效响应。如果业务上需要给个提示换成return 421也可以。Apache 的写法对应的_default_虚拟主机VirtualHost _default_:80 ServerName catchall.invalid Redirect 404 / /VirtualHost VirtualHost *:80 ServerName www.example.com ServerAlias example.com DocumentRoot /var/www/example /VirtualHost5.2 应用框架的开关别关掉很多框架本身就带 Host 校验只是被开发者为了方便关掉了。Django 的ALLOWED_HOSTS是最典型的例子配成[*]就等于全放开。生产环境必须是明确列表ALLOWED_HOSTS [ www.example.com, example.com, api.example.com, ]Spring Boot 这边如果部署在 Nginx 后面关键是让应用只信任来自可信上游的转发头。常用做法是配置server.forward-headers-strategynone由 Nginx 统一把 Host 规范好再转发避免应用读了外层伪造的头。如果确实需要读转发头就要配合RemoteIpValve的internalProxies白名单。Express 这类 Node 框架没有内置 Host 校验需要自己加中间件const ALLOWED_HOSTS new Set([ www.example.com, example.com, api.example.com, ]); app.use((req, res, next) { const raw req.headers.host || ; const hostname raw.split(:)[0].toLowerCase(); if (!ALLOWED_HOSTS.has(hostname)) { return res.status(400).send(Invalid Host header); } next(); });这个中间件要放在所有路由之前。有一个细节容易被忽略如果服务同时在 HTTP 和 HTTPS 上监听端口号会被带进req.headers.host所以按冒号切分取主机名是必要的。5.3 业务代码里不能再出现的东西修复这类漏洞配置只是一半代码里那些顺手拿请求头的写法也得清掉。我整理了一份替换对照可以直接拿去 code review 用危险写法风险推荐替换req.headers.host拼链接重置链接投毒读取配置的PUBLIC_BASE_URLrequest.getHeader(X-Forwarded-Host)直接用伪造头生效入口层清洗后注入应用只读内部变量Location: // 用户输入协议相对跳转被劫持白名单校验跳转目标路径缓存键用$host不加规范化缓存投毒缓存键只用规范化后的站点标识日志里直接写 Host 原文日志污染写入前做字符过滤和长度截断代码 review 的时候我会直接全局搜索这几个关键词headers.host、X-Forwarded-Host、build_absolute_uri、getHost()。搜出来的地方逐个看用途凡是用来生成对外链接或做权限判断的一律要求改配置化。5.4 反向代理与缓存层的加固细节如果是多级架构CDN 到 Nginx 到应用每一层的头处理都要对齐。我一般遵循一个原则入口层负责清洗内部层只信任上游。具体来说CDN 层要配置把客户端传入的X-Forwarded-Host、X-Original-Host等彻底覆盖掉由 CDN 自己重新写入。Nginx 层收到后只保留一份规范化的 Host转发给应用时用proxy_set_header Host $host注意$host是经过 Nginx 处理的小写主机名比$http_host更安全后者是客户端原始值。缓存配置里Nginx 的proxy_cache_key如果只写$request_uri不同域名的内容会互相串。默认的$scheme$proxy_host$request_uri相对安全但前提是proxy_host是固定的上游地址。真要精细控制可以显式指定proxy_cache_key $scheme$server_name$request_uri;这里用$server_name而不是$host因为它是配置里写死的不受请求影响。这个细节我在排查缓存投毒时踩过坑改完记得清一次缓存再验证。6. 常见问题速查与几个踩过的坑6.1 误报漏报对照表现象可能原因处理动作换 Host 返回 200页面无回显单站点默认落点加default_server兜底标注为配置加固项只有Location回显伪造域名跳转未白名单校验跳转目标改用相对路径或固定域名重置邮件链接域名可控业务代码用请求头拼链接改为配置项回归测试邮件正文换 Host 无反应换X-Forwarded-Host有反应应用信任转发头修入口层清洗规则 上游白名单修改后仍被扫描器报出只改了入口层应用仍拼 Host全链路搜索代码关键字返回 444 但扫描器仍报扫描器按响应长度判定附上验证报文和截图走漏洞豁免流程6.2 上线后复测被打回的三个常见原因第一个原因是只堵了一半入口。HTTP 的 80 端口配了default_serverHTTPS 的 443 没配扫描器走 HTTPS 照样命中。这个错误我犯过一次改的时候两个listen都要带上default_server。第二个原因是测试环境没同步。生产修复了预发环境的配置还是老的每次发版前扫描预发都会重新报出来。建议把 Nginx 配置纳入版本管理三个环境同一份配置模板只替换域名变量。第三个原因是应用层白名单写得太宽。有团队为了省事写成后缀匹配*.example.com结果evil.example.com也过了。如果是后缀匹配还要额外校验域名归属复杂度反而更高不如老老实实列全。6.3 我个人在处理这类漏洞时的几条经验第一条先看有没有凭据流转再决定修复优先级。同样是 Host 头被信任出现在页面底部的一个分享链接里和出现在密码重置邮件里紧急程度完全不同。我一般把资产按是否涉及账号体系分两批处理涉及账号的当天改完。第二条修复后一定要拿原始报文回归一遍。不要只看扫描器绿了就收工。我自己会准备一个小的回归清单把 Host、X-Forwarded-Host、X-Host、带端口的 Host、带特殊字符的 Host 这几组都跑一遍确认全部被拒或全部返回固定域名。第三条把这次暴露的写法收进团队编码规范。Host 头攻击的本质是用了不该信的输入同类问题还有 Referer 拼接、Origin 判断、X-Forwarded-For 做限流依据。修完这一个点顺手在团队里把请求头一律视为不可信输入这条写进 code review checklistROI 比单修一个漏洞高得多。第四条关于扫描器报告的沟通技巧如果确认是误报比如服务端对所有 Host 返回同一默认站点但无任何回显不要简单标忽略。我会在报告里附上三条对比报文和一次 DNSLog 验证结果说明该配置属于健壮性建议而非可利用漏洞这样审核方更容易接受。最后再分享一个排查小技巧。当你怀疑某个值被应用信了但页面上看不到回显的时候可以在 Host 里塞一个自动生成的子域名类似probe-随机串.your-dnslog-domain然后观察有没有 DNS 解析记录。服务器只要用它做过任何形式的地址拼接并尝试发起请求解析记录就会出现。这个方法对判断缓存投毒和重置链接投毒特别有效比反复翻代码快得多。整个过程只针对自己的资产做边界要守住。
返回列表