同一个网址,电脑上打开安然无恙,手机上一打开就跳到陌生页面,甚至跳完还回不去原页面,这是最容易被误判的一类故障。手机网页打开就跳转电脑却正常,先别怀疑是手机中毒,问题大概率在网站这一侧。手机网页被挂马怎么办,核心就一句:用移动端的身份去抓一遍页面,把劫持位置按层级钉死,再动手清理。
一、先把范围缩到最小
连着做四次访问,范围就能收得很紧。第一次,手机连 WiFi 打开,看是否跳转;第二次,同一台手机切到流量打开,如果换成流量就不跳了,问题在 WiFi 那条链路或者运营商,不在网站;第三次,换另一台不同品牌的手机打开,两台都跳说明是服务端统一下发的,只有一台跳要查那台设备本身;第四次,用电脑把浏览器 UA 手改成手机再打开,如果电脑上也跳了,就确认是服务端按 UA 分发内容,和手机硬件无关。四次测完,责任方基本明确。
二、用两个 UA 对比响应,差异一眼就能看见
确认是服务端的问题之后,要拿到可对比的证据。下面这段脚本对同一个地址分别用手机和电脑的 User-Agent 各请求一次,打印两次响应的长度、页面标题,并列出两者不相同的行,最后统计页面里跳转特征出现的次数。把 URL 改成本次出问题的页面地址,脚本跑完你会直观看到两份页面差在哪。
# 同一个地址用手机/电脑两种 User-Agent 抓取,对比响应差异
import difflib, re, urllib.request
URL = "https://www.example.com/" # 改成出问题的页面地址
UA_PC = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36"
UA_MOBILE = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1"
JUMP = re.compile(rb"window\.location|document\.location|http-equiv=[\"']?refresh", re.I)
def fetch(ua):
req = urllib.request.Request(URL, headers={"User-Agent": ua})
return urllib.request.urlopen(req, timeout=15).read()
pc, mob = fetch(UA_PC), fetch(UA_MOBILE)
title = re.compile(rb"<title[^>]*>(.*?)", re.S | re.I)
print("电脑端 长度 %d 标题 %s" % (len(pc), (title.search(pc) or [b"", b"-"])[1].decode("utf-8", "ignore")))
print("手机端 长度 %d 标题 %s" % (len(mob), (title.search(mob) or [b"", b"-"])[1].decode("utf-8", "ignore")))
print("跳转特征次数 电脑端 %d / 手机端 %d" % (len(JUMP.findall(pc)), len(JUMP.findall(mob))))
a = pc.decode("utf-8", "ignore").splitlines()
b = mob.decode("utf-8", "ignore").splitlines()
for line in list(difflib.unified_diff(a, b, lineterm=""))[:40]:
print(line[:160])
结果里有三种典型形态。长度和标题都一样、只有零星差异,说明劫持不在页面本身,往 CDN 缓存或者链路插入上查。手机端长度明显变长、多出一段脚本,说明页面被注入了内容,重点查移动端模板和公共 JS。两份页面完全不同、手机端直接是个陌生页面,说明程序在入口处就按 UA 做了跳转,重点查入口文件和框架中间件。
三、按差异类型定位到具体文件
定位的手法就一招:拿命中的特征串去全站文件里搜。先搜移动端模板目录和公共 JS,命中率最高;再搜入口文件和框架的路由、中间件文件;程序里搜不到,就去看 Nginx 的配置文件有没有按 UA 写的 if 判断或 rewrite 规则。数据库也别漏,跳转地址有时存在配置表里,改模板和文件都白改。访问日志这时可以派上用场:按出错页面的路径筛日志,看跳转发生的时间段和请求来源,能帮你判断是最近才被改的,还是已经存在一段时间。
四、清理、加固与真机复检
清理前先全量备份,再按文件修改时间排序,把最近动过的文件逐个还原成干净版本;用了 CDN 的刷新全站缓存;改掉后台、数据库、服务器三处密码;给页面响应头加上内容安全策略,限制脚本来源。复检必须用真机,安卓和 iOS 各一台,同时把上面的双 UA 脚本再跑一遍,确认手机端和电脑端的响应恢复一致。之后把这条脚本挂进定时任务,每天跑一次,跳转特征次数出现差异就告警——移动端劫持最怕的不是清理麻烦,而是根本没人发现它又回来了。
相关阅读:
A5创业网 版权所有