网站偶尔跳到博彩页,刷新就好,代码里查不到木马:六个环节逐段排除

用户用手机流量访问时偶发跳转博彩页、刷新即恢复、代码里查不到木马——这三个特征几乎可以排除掉一半可能。本文按命中率拆解六个环节:运营商 HTTP 劫持、移动 UA 定向木马、第三方脚本投毒、DNS 劫持、伪基站、CDN 缓存污染,给出逐段自查命令与一张排查流程图。

「好几个客户反馈,用手机流量访问网站时,会突然跳到一个卖彩票的页面,刷新一下又好了。查了代码没发现木马。」

这个描述信息量其实很大,大到几乎可以排除掉一半的可能:

  • 偶发。刷新就好,说明不是站内代码每天在执行。
  • 手机流量。不是 WiFi,不是所有网络环境。
  • 代码里没木马。文件层面可能是干净的。
  • 跳到博彩。劫持类流量的典型变现方向。

四条合在一起,指向的往往不是「网站被黑了」,而是页面在从你服务器出来之后、到用户手机屏幕之前,某一段路被人动了手脚。这段路可能有五六个环节,每个环节的排查方法都不一样。

这篇按「可能性从高到低」拆一遍,每一步都给可直接执行的命令。

网站偶发跳转博彩广告的六个可能环节:源站被黑、DNS 劫持、运营商插广告、CDN 缓存污染、用户端热点与伪基站
用户看到「跳了一下」,中间其实有五六个环节可能被人插手。排查就是逐段排除。

一、先分清:是谁的页面在跳?

排查第一步不是查代码,是看跳转到底怎么发生的。三种形态,指向完全不同的方向:

现象 技术本质 大概率的成因
HTTP 302/307 跳转,地址栏 URL 变了 服务端或链路中间返回重定向 运营商劫持、CDN 配置被改、伪基站
页面先正常显示,几秒后被顶走 页面里执行了 location.href= 被注入 JS、第三方脚本被投毒
出现一个浮层/新窗口广告,原页面还在 iframe 或新窗口打开 DNS 被劫持到代理页、运营商插广告

怎么判断?用电脑 Chrome 打开手机模拟模式,开网络面板 + 保留日志,勾上 Preserve log,然后疯狂刷新直到复现。看第一条「跳出去」的请求:

  • 如果 Document 请求的响应码是 302,看 Location 头指向哪;
  • 如果响应码是 200 但页面被替换了,看 Response 里有没有陌生的 <script>;
  • 如果根本没发新请求、只是 URL 自己变了,多半是 JS 干的或浏览器层被插了东西。

记住一个判断原则:能复现就查链路,不能复现就查环境。偶发的劫持之所以难查,是因为它根本不发生在你的服务器上。

如果你已经确认是站内注入这一路(第三、四节会细讲),我们之前写过一篇更完整的实战复盘,可以先看那篇:《网站被入侵跳转到非法链接?5 步自查 + 百度云防护方案》。本文重点补的是「站内干净、但还是跳」的那一半。

二、六个环节,按命中率排序

1. 运营商 HTTP 劫持(命中率最高)

这是「手机流量」四个字最直接的指向。部分地区的移动网络会对明文 HTTP 流量做旁路分析,一旦识别出页面特征,就插入广告脚本,或者直接 302 到推广页。

为什么刷新就好?因为劫持是概率性投放的——按规则抽样,不是每个请求都拦。这也是它最难查的原因。

自查方法很土但有效:用手机流量连续访问 20 次你的首页,每次记录是否跳转;同时用 https 再试 20 次。如果 HTTP 跳、HTTPS 不跳,基本可以定性。

# 对比两次响应体是否被篡改:抓首页源码,搜可疑注入
curl -s http://www.example.com/ | grep -nE '<script[^>]*src="https?://(?!www\.example\.com)'

# 看响应头有没有被中间设备加上奇怪的标记(有些劫持会带 X-Cache 之类的伪造头)
curl -sI http://www.example.com/ | grep -iE 'via|x-cache|x-cnection|server'

# 用不同网络出口各测一次,对比首字节和内容长度是否一致
for i in 1 2 3; do curl -s -o /dev/null -w "%{size_download} %{time_total}\n" http://www.example.com/; done

注意最后那条命令:同一份页面,多次请求返回的大小不一样,就是一个很强的信号。

2. 网站真的被黑了,只是木马藏得深

「查了代码没发现木马」不等于没有。现在的主流手法早就不往 index.php 里塞 eval 了:

  • 加密混淆:一长串 base64_decode 嵌套,肉眼看着像正常数据;
  • 隐藏在正常文件尾部:在图片、JS、CSS 末尾追加一段;
  • 只对移动端 UA 输出:PC 上访问完全正常,手机 UA 才触发——这正好解释了「为什么只在手机上跳」;
  • 数据库注入:直接改 wp_options 或页面表的字段,文件层面永远是干净的。

所以排查要换思路:不看代码长得像不像木马,看它「不应该出现在哪」。

# ① 找近 30 天被改过的 PHP / JS 文件(排除缓存与上传目录)
find /www/wwwroot/example.com -type f \( -name "*.php" -o -name "*.js" \) \
-mtime -30 ! -path "*/cache/*" ! -path "*/uploads/*" -printf "%TY-%Tm-%Td %p\n" | sort -r | head -40

# ② 找文件末尾被追加内容的(正常 PHP 不会以 base64 结尾)
grep -rlE "eval\(|assert\(|base64_decode\(|gzinflate\(|str_rot13\(" \
/www/wwwroot/example.com --include="*.php" | head -30

# ③ 找隐藏文件(点开头的 PHP 在 Web 目录里极不正常)
find /www/wwwroot/example.com -name ".*.php" -o -name "*.php.*" | head

# ④ 看有没有最近新增的可疑文件(webshell 通常体积很小)
find /www/wwwroot/example.com -name "*.php" -size -4k -mtime -30 -printf "%s\t%p\n"

数据库侧也要查一遍。WordPress 站直接跑:

# 查选项表里有没有被塞进脚本(劫持代码最爱藏在 wp_options 和文章正文里)
mysql -uroot -p 库名 -e "SELECT option_name FROM wp_options WHERE option_value LIKE '%<script%';"
mysql -uroot -p 库名 -e "SELECT ID FROM wp_posts WHERE post_content LIKE '%location.href%' OR post_content LIKE '%atob(%';"

# 查有没有多出来的管理员账号
mysql -uroot -p 库名 -e "SELECT ID,user_login,user_email,user_registered FROM wp_users ORDER BY ID DESC LIMIT 10;"

这里有个关键点:很多劫持只对移动 UA 生效,所以 PC 上打开一切正常。验证方法很简单——用 UA 伪装请求一次:

# 用 iPhone UA 取一次首页,和正常 UA 的响应体做 diff
curl -s -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15" \
https://www.example.com/ > /tmp/mobile.html
curl -s https://www.example.com/ > /tmp/pc.html
diff <(wc -c < /tmp/pc.html) <(wc -c < /tmp/mobile.html)
grep -oE '<script[^>]+' /tmp/mobile.html | grep -viE 'example\.com|google|baidu'

两份响应的长度差太多、或者 mobile 版多出外链脚本,问题就在服务端。

3. 第三方脚本 / 广告位被投毒

如果你接了统计、客服、广告联盟、字体 CDN,任何一个供应商被入侵,注入的 JS 都会在你域名下执行。注意看手机的响应体里,外链脚本的域名是不是你认识的。

更隐蔽的是「组件供应链」:某个统计代码某天开始加载一个二次下发脚本,只对移动端下发、只对特定地区下发。这类问题在 PC 上完全看不出来。

处置:把外链脚本全部列出来,逐个确认来源;不用的立刻删掉,必须用的加 defer 并考虑 SRI 校验。

4. DNS 被劫持

DNS 劫持的特征是「连你的服务器都没碰到」——用户解析到的根本不是你。判断方法:

# 用多个公共 DNS 分别解析,结果应该完全一致
for dns in 223.5.5.5 119.29.29.29 8.8.8.8 114.114.114.114; do
echo -n "$dns -> "; dig +short @$dns www.example.com A | tr '\n' ' '; echo
done

# 和权威 DNS(你的域名服务商)的记录对比
dig +short @ns1.your-dns-provider.com www.example.com A

如果公共 DNS 的结果和权威记录不一致,或者不同 DNS 给出不同结果,就是 DNS 层的劫持。这种情况换 DNS 服务商、上 DNSSEC 是最直接的解法。

5. 伪基站 / 免费 WiFi 网关

只影响特定环境:某个商场 WiFi、某个地区的 2G/3G 网络。特征是「同一个人,换个地方就好了」。

这类劫持你没法从服务器端解决,能做的只有一件事:让页面必须走 HTTPS,并对 HTTP 请求做 301 强制跳转。明文传输是这类劫持唯一的入口。

6. CDN / 反向代理缓存被污染

如果你用了 CDN,某个边缘节点缓存了一份被注入的 HTML,那么命中这个节点的用户就会看到广告,直到缓存过期。「刷新就好」有时候是因为刷新打到了另一个节点。

处置:CDN 控制台里对该 URL 做「刷新缓存」并开启「忽略参数缓存」检查;如果反复出现,说明注入源在回源链路上,要继续往下查。

三、一张排查流程图

步骤 动作 结论方向
1 HTTPS 与 HTTP 各测 20 次,记录跳转率 只有 HTTP 跳 → 运营商/热点劫持
2 多个公共 DNS 解析对比 结果不一致 → DNS 劫持
3 用移动 UA 抓源码,与 PC 版 diff 响应体不同 → 服务端注入或被黑
4 find -mtime 查 30 天内改动文件 有陌生文件 → 已被入侵
5 查数据库 option 与文章正文 有外链脚本 → 数据库注入
6 CDN 控制台刷新缓存并观察 刷新后恢复 → 缓存污染
7 更换网络环境(WiFi / 4G / 异地)复测 换环境不复现 → 本地网络劫持

按这个顺序走,一般三十分钟内能定位到具体环节。

四、修复:分「治标」和「治本」两条腿

治本一:全站 HTTPS,不留明文入口

# /www/server/panel/vhost/nginx/example.com.conf
server {
listen 80;
server_name www.example.com example.com;
return 301 https://$host$request_uri; # 不留任何明文页面
}

server {
listen 443 ssl http2;
server_name www.example.com example.com;
ssl_certificate /www/server/panel/vhost/cert/example.com/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
...
}

宝塔路径:网站 → 设置 → SSL → 强制 HTTPS,再在「配置文件」里确认没有残留的明文跳转白名单。

治本二:CSP 限制外部脚本

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://hm.baidu.com; img-src 'self' data: https:; frame-ancestors 'self'" always; # 部署
nginx -t && nginx -s reload

CSP 不能防服务端输出被篡改,但能挡住「页面被注入了陌生 JS」这一大类——外链域名不在白名单里,浏览器直接拒绝执行。建议先用 Content-Security-Policy-Report-Only 跑一周看报告,别一上来就硬拦。

治标:临时加固顺序

  1. 清理可疑文件与数据库注入,改所有管理员密码;
  2. 关闭 xmlrpc.php、限制 /wp-admin 访问来源;
  3. 开启网站文件防篡改(宝塔有内置,或 chmod 444 保护核心文件);
  4. 配置页面完整性监控,文件被改立刻告警。

五、为什么「偶发 + 查不到木马」最难治

把这几种情况放一起对比,会发现一个规律:

类型 你能否从服务器端看到 能否靠改代码解决 能否靠设备防护解决
代码被注入木马 能 能 不需要
数据库注入 能(查库) 能 不需要
DNS 劫持 不能 不能 不能
运营商 HTTP 劫持 不能 不能 不能
WiFi / 伪基站 不能 不能 不能
CDN 缓存污染 部分 不能 不需要

后三类占了「偶发跳转」的大多数,而它们的共同点是:问题不发生在你的服务器上,你在服务器上怎么找都找不到。

更麻烦的是,只有 HTTPS 是不够的。HTTPS 能挡住明文劫持,但挡不住「源站已经被拿了」这种情况——比如某个老插件有 RCE、后台弱口令被爆破、或者数据库配置暴露在公网。这类入侵往往做完就删日志、留一个只在手机 UA 下触发的后门,你查一个月也查不出来。

所以实际防护要分成两层:一层管「流量在路上别被改」,一层管「请求进站前先被筛一遍」。

六、加一层「入口级」的过滤,比事后排查省事

把这几年的经验总结成一句:劫持类问题,事后排查的成本远高于事前过滤。因为偶发问题的复现成本极高,而流量一旦进了源站,日志里你看到的往往只是「一个正常请求」。

真正有效的做法是在流量进源站之前就做一次判断和清洗,也就是常说的云 WAF 接入。这里不做硬推,只说几个和你这个问题直接相关的功能点:

  • 隐藏源站。域名用 CNAME 接进云端后,公网只看到防护节点的 IP,源站不再暴露。扫描器找不到源站,爆破和漏洞利用就少了一大半入口。
  • 恶意代码注入检测。WAF 规则库里有一类专门检测响应内容与请求特征的规则,「页面被插入陌生 <script>」「页面嵌入可疑 iframe」这类问题会在响应侧被识别并阻断。
  • 网页防篡改。把核心页面在云端做缓存副本,源站哪怕被改,返回给用户的也是干净页面——这正好对应「源站被黑,但你自己还没发现」那个场景。
  • 全站 HTTPS 与证书托管。WAF 节点上可以直接签发并托管证书,不用在源站折腾,HTTP 强制跳转也在节点层完成。
  • Bot 与异常访问识别。爆破后台、批量探测、异常 UA 这些行为在 WAF 侧有现成的画像能力,比在服务器上一条条写规则轻松得多。

百度云防护在这几条上是比较齐全的:SaaS 接入只改 DNS、不动服务器架构,对一个已经在跑的老站来说改动最小;0day 类漏洞有虚拟补丁,能在你升级程序之前先挡住;源站隐藏 + 回源 IP 白名单这套组合,能把「源站裸奔」这个最大的风险直接掐掉。

但也要说清楚边界:WAF 治不了运营商在中间插广告。那是链路问题,只能靠 HTTPS 和 DNS 安全来解决。WAF 治的是「源站被人拿到」这一类——而这类恰恰是危害最大的,因为它不只跳广告,还会拖库、挂马、被挂到黑产分销页。

七、给站长的 10 分钟自查清单

项 怎么做 判断标准
HTTP 是否还有明文入口 curl -I http://你的域名 必须是 301 到 https
HSTS 是否开启 curl -I https://你的域名 有 Strict-Transport-Security 头
移动 UA 响应是否异常 用 iPhone UA 抓源码 diff 两份响应体的脚本列表一致
近期文件改动 find -mtime -30 无陌生文件
数据库是否有注入 查 options / posts 表 无未知外链脚本
多 DNS 解析一致 4 个公共 DNS 对比 结果完全相同
CDN 缓存策略 控制台看 HTML 的 TTL HTML 不缓存或短缓存
源站是否暴露 用第三方工具查域名历史解析 查不到真实 IP 最优
后台入口 /wp-admin 是否对公网开放 加 IP 白名单或二次验证
日志留多久 看 Nginx access log 保留策略 ≥ 180 天

八、写在最后

「偶尔跳转、刷新就好、查不到木马」——这三个特征凑在一起,八成不是你的代码出了问题。

但也别因为「不是我的问题」就放着不管。用户记不住是谁劫持的,只记得「上这个站会跳广告」,最后掉的是你的品牌和信任。尤其是做企业站的,客户第一次访问就撞上博彩页,这一单基本没了。

能做的其实很清楚:把明文入口全部关掉、把源站藏起来、把入口流量筛一遍、把文件与页面盯起来。前两件今天就该做完,后两件是长期的事。

本文的排查命令适用于 Linux + Nginx + MySQL 环境(宝塔面板通用),Windows IIS 站点可按同样思路替换为对应的 PowerShell 命令。

给TA打赏
共{{data.count}}人
人已打赏
在线客服
在线客服
热线电话
QQ客服
电子邮箱
suduwangluo