那天傍晚,SOC平台弹出一条高优先级告警:某台对外业务的Web服务器在过去的3小时里,每隔30分钟向一个海外IP发起一次HTTP外联,每次流量1到3KB。说实话,第一反应是挖矿木马——这种定时、小流量、到固定IP的外联模式,太像矿机在向矿池汇报挖矿结果了。但登上服务器翻了一圈,进程列表干干净净,CPU使用率也平稳得不像话,crontab、systemd定时器、网络连接、登录日志全部正常。于是把流量导出来准备抓包细看,结果发现根本不是矿池通信,而是一个Webshell在周期性接收外部指令,并回显命令执行结果。
这篇文章就记录从怀疑到确认,再到处置加固的完整过程。重点会放在流量分析的方法论上:哪些字段最可疑、如何从pcap里把一条命令执行的完整链路捞出来、安全设备为什么没拦住、处置时又应该按什么顺序做才不遗漏证据。如果你也需要面对类似告警,或者想提前知道Webshell流量长什么样,这篇文章应该能帮上忙。
1. 不是挖矿木马,是Webshell在做外联
1.1 从告警信息里能读出哪些有用情报
告警是安全设备给的,但设备只会告诉你“有异常”,不会告诉你“发生了什么”。所以拿到告警的第一件事,是把原始信息拆开看。我当时从SOC平台拉到了下面这组字段:
| 字段 | 值 | 分析价值 |
|---|---|---|
| 源IP | 10.x.x.35(业务服务器内网IP) | 确认是我们要保护的资产 |
| 目的IP | 45.x.x.x(海外) | 跨地域外联,明显不符合业务需要 |
| 目的端口 | 80 | 不是常见矿池端口,下面细说 |
| 应用协议 | HTTP | 与业务对外提供的Web服务端口一致 |
| 发生次数 | 约6次/3小时 | 频率固定,说明是自动化任务 |
| 单次流量 | 上传约1KB,下载约2~4KB | 体量很小但双向都有数据 |
很多人看到“海外IP+定时外联”就自动归为矿池通信,但这里有两个细节不对劲。第一,常见矿池端口是3333、4444、14444、5555这类非标准端口,走80端口且协议是HTTP的矿池不是没有,但很少;第二,矿机上行数据虽然小,但下行数据通常更小甚至几乎为0,因为矿池只需要向矿机下发任务和难度调整,没必要每次回传2~4KB。而一个Webshell在等待指令时,请求体和响应体往往都有实际内容——请求体里是命令参数,响应体里是命令输出。这个双向流量特征,是我后续决定抓包分析的最重要原因。
1.2 先排查主机侧异常:为什么一开始查不到
拿到告警后,我按常规顺序检查了服务器主机本身。不看不知道,看了之后更疑惑。
top、ps aux:CPU正常,进程列表只有php-fpm、nginx、mysql等常规服务,没有看到任何可疑进程名。netstat -antlp:正在建立的连接里确实有到45.x.x.x:80的,但对应的进程PID是php-fpm,这在Web服务器上太正常了——php-fpm本来就要处理HTTP请求,攻击者只要让Webshell以Web请求的方式发外联,进程层面就永远不会亮红灯。crontab -l、systemctl list-timers:没有发现新增的定时任务。last、/var/log/auth.log:没有异常登录记录,也没有SSH爆破成功的迹象。lsmod、/proc/:没有加载可疑内核模块,隐藏进程也没扫出来。
也就是说,主机侧检查看起来“一切正常”,但网络侧明明有可疑外联,这个矛盾本身就说明问题不在系统层,而在Web应用层。Webshell是寄生在PHP-FPM进程里的,它不注册服务、不写定时任务、不弹shell,全靠HTTP请求来驱动,所以传统的进程排查、登录日志排查、rootkit扫描对它基本无效。这也是为什么流量分析在Webshell排查里是不可替代的手段——你查不到一个不存在的进程,但所有行为都会在网络上留下痕迹。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从pcap里捞出完整请求链:三个关键特征
2.1 第一步:把可疑外联流量完整抓下来
主机侧没结果,我决定回到网络侧,直接抓包。这里有个前提:最好在安全设备或交换机镜像口上抓,而不是直接在业务服务器上抓。如果Webshell已经带有检测Sandbox或反调试逻辑,你在一台已经失陷的机器上执行抓包命令,理论上攻击者是有可能感知到的。不过当时现场条件有限,我是在出口防火墙镜像口上抓的,这个位置最不影响业务,也最接近攻击者视角。
抓包命令很简单:
bash复制tcpdump -i eth0 -s 0 -w webshell.pcap host 10.x.x.35 and port 80
为了不把整个业务流量全导进来,我在命令里限定了源IP和端口。-s 0表示抓完整包,不要截断,因为后面要还原HTTP响应体里的命令回显,如果只抓了每个包的前96字节,就什么都看不到了。抓了大概40分钟,pcap文件体积不到20MB,对这个量级来说非常小,也侧面印证了外联流量确实很轻。
然后把pcap文件拉回本地分析环境,用tshark做了第一轮HTTP请求汇总:
bash复制tshark -r webshell.pcap -Y "http.request" -T fields \
-e frame.time \
-e ip.src -e ip.dst \
-e http.request.method \
-e http.request.uri \
-e http.user_agent \
-e http.content_length
几秒后输出结果就让我心里基本有数了。这个“海外IP”访问的URI根本不是业务的正常路径,而是/uploads/avatar_20240812_142313.jpg.php这种带双重后缀的可疑文件,并且请求方法全是POST。业务里没有理由让用户POST访问头像文件,这个URI完全可以列为重点可疑对象。
2.2 三个特征:请求头、请求体、响应体的组合判断
确认了目标URI后,我把Wireshark里对应的TCP流用Follow TCP Stream完整还原出来,发现这个Webshell的连接流量有三个特征,单独拿出任何一个都不是铁证,但组合在一起基本可以直接定性。
第一个特征是User-Agent和Accept相关字段对不上。攻击者把UA伪装成了Chrome浏览器,看起来是“Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36”这一串,但请求头里没有常见的Accept-Language字段,Accept-Encoding也缺失。正常浏览器即使不做任何额外配置,也会在请求里带上这些字段,缺失本身就说明这不是浏览器发出的请求,而是某个脚本或者框架在模拟浏览器。
第二个特征是POST请求体是纯编码内容。请求体里没有常见的表单字段,只有一个规则外的自定义参数名,参数值是一长串Base64字符串。正常业务表单提交的数据通常包含username=xxx、file=xxx这类可读键值;而Webshell为了传输命令又不被明文特征匹配到,最常见的做法就是把命令序列化之后Base64编码塞进一个参数里——字符串结尾频繁出现=填充符,且整个值域只有大小写字母和数字、+、/、=,是典型的Base64字符集。
第三个特征是响应体的长度和请求体高度相关。用tshark按http.response过滤后可以看到,服务器对每一个可疑POST请求都会在几百毫秒内返回一个长度与请求体相近的HTTP 200响应。这很关键:如果是静态资源请求,响应内容基本是固定长度的文件内容,和请求参数没有任何关系;如果是登录、查询等正常接口,响应结构也会相对固定。只有“命令执行+回显”类Webshell,响应体才会直接由请求体里的命令结果决定长度。攻击者发一个几十字节的编码参数,服务器返回几十字节的执行结果,一来一回,特征非常整齐。
我把三个特征整理成了一张对照表,方便后面写检测规则时直接用:
| 维度 | 正常业务POST请求 | 本次可疑请求 |
|---|---|---|
| UA | Chrome/Firefox完整请求头 | 只有UA字段,缺少Accept-Language等 |
| Content-Type | application/x-www-form-urlencoded或multipart/form-data | 未声明或与内容不符 |
| 请求体 | 可读表单键值 | 单一参数,值为Base64长串 |
| 响应长度 | 固定产品或固定结构 | 随请求体变化,长度近似 |
| 请求频率 | 用户点击驱动,不规律 | 固定30分钟一次,机器人周期 |
2.3 再往下挖一层:解码后能看到什么
光是看到Base64还不够,我需要对请求体做一个还原,确认里面到底是不是命令。我用tshark把POST参数值单独导出,然后用Base64解码:
bash复制tshark -r webshell.pcap -Y "http.request" -T fields -e http.file_data | \
sed 's/^.*param=//' | base64 -d
解码结果里出现的是对当前身份、当前目录、系统位置的探测指令。说实话,到这里已经不是“高度可疑”了,而是可以确认:这个上传目录下的PHP文件就是一个远端命令执行的Webshell,攻击者正在通过HTTP通道对这台服务器下发命令,命令结果原样回传。后面我还在后续几个请求里看到了读取配置文件、列目录、检查内网连通性的指令,但因为想着要给安全处置留证据,我没有继续往下解更多指令,避免自己在生产环境里“帮攻击者执行命令”。
3. 还原攻击行为:他到底在这台机器上做了什么
3.1 时间线还原:从扫描到上传到回连
确认是Webshell以后,下一步不是立刻删文件,而是先把整个攻击链路的来龙去脉在流量里还原出来。我把同一个源IP在事件前后几小时内的所有请求按时间排了序,整理出这样一条时间线:
- 14:02:源IP对业务根路径发起了一轮高频路径扫描,命中了一个旧版上传接口
/upload/avatar.php。这个接口不在当前业务导航里,但服务端没有对废弃接口做下线处理。 - 14:17:攻击者通过该上传接口提交了一个multipart/form-data请求,文件名是
avatar_20240812_142313.jpg.php。从名字上看,是在尝试用双重扩展名绕过前端后缀检查。 - 14:23:第一次访问
/uploads/avatar_20240812_142313.jpg.php并带上POST参数,服务器返回200且响应体里包含系统用户信息,攻击者确认上传的Webshell已经生效。 - 14:23到17:50之间:Webshell停止高频交互,改为每30分钟回连一次,每次执行一到两个简单命令。
- 17:58:SOC平台产生外联告警。
这里的重点是14:23到17:50之间那段时间。如果只看SOC告警,你大概率只会注意“定时外联”这一个问题;但回到流量流水里才会发现,真正危险的动作发生在最初的几分钟里——上传、验证、提权、信息收集都挤在这一小段窗口内。之后的定时回连不过是维持控制通道,反倒因为节奏太规律而被检测系统捕获。所以分析Webshell事件时,不要只盯着告警时段看,要把时间轴往前拨到第一次访问那个可疑URI的时刻。
3.2 命令执行回显里能看到什么操作类型
解码了部分请求参数后,我按命令的类型给攻击者的行为做了个聚类,没有去深挖每一条命令的具体输出,但总结出来大概四类:
- 身份确认类:查看当前Web进程用户、系统发行版信息、内核版本。这类命令通常是刚拿下Webshell后的第一步,用来判断后续怎么提权、怎么横向。
- 路径探测类:查看当前目录、列目录、读取相对路径下的配置文件。这个动作跟他在寻找其他敏感信息是吻合的。
- 网络侦察类:查看本机网络连接、检查内网主机连通性。说明攻击者在顺手摸内网拓扑。
- 凭据收集类:尝试读取一些常见的配置文件和密钥文件。
需要特别提醒的是,做这种分析时一定要克制。看到命令执行结果之后,不要顺手再用Webshell去执行更多验证命令,因为每一次执行都会被攻击者控制通道里看到,也可能触发更多未知的持久化逻辑。我当时的做法是,解码只做到“确认操作类型”这一层,必要的信息全部从pcap和日志里提取,不再对目标服务器发起任何主动命令。
3.3 日志交叉验证:Web访问日志之外的痕迹
流量能告诉我们“发生了”,但有些细节还需要配合服务端日志来确定,比如上传的文件到底是存在哪个目录、上传时带了什么Cookie、是哪个账号上传的。我翻了这么几类日志:
- Nginx/Apache访问日志:确认了14:17的上传请求和14:23的首次访问记录都存在,状态码均为200,响应字节数和pcap里看到的完全一致。
- PHP-FPM慢日志:发现这个可疑PHP文件在14:23到14:30之间出现了一次超长执行记录。正常业务脚本都是毫秒级完成,这几百毫秒的异常执行时间进一步佐证了它在运行外部命令等待回显。
- 登录日志:没有发现除了运维账号以外的SSH登录成功记录,初步判定攻击入口不在SSH层面。
- 系统audit日志:没有与之相关的规则,所以这层证据是缺失的。也是这件事之后,我建议在关键业务服务器上把文件写入审计打开,尤其是Web目录。
日志交叉验证的目的不是为了“多一层确认”,而是为了回答几个流量回答不了的问题:攻击者上传时有没有登录态?是哪个用户上传的?这个文件是不是唯一一个?只靠网络流量,你永远无法完全解释“业务侧到底是从哪个漏洞进来的”,服务端日志补上了这一块拼图。
4. 处置顺序与修复动作:先止血,再清创,最后上监控
4.1 第一步隔离与取证:不急着删文件
很多人发现Webshell后的第一反应是“赶紧删掉”,但这其实是大忌。文件删掉之后,文件的inode、上传时间、全路径、文件属性都拿不到了,后续审计时会很被动。我这次的处置顺序是严格按照“先止血、再取证、再清创、最后修复”来的:
- 阻断网络通道。通过云安全组或者防火墙,先把服务器到45.x.x.x这个目标IP的所有流量临时阻断。这一步不影响正常业务,但能立刻掐断攻击者的控制通道,优先级最高。
- 保留现场证据。给云硬盘打快照,同时把之前抓到的pcap文件单独加密备份,记录抓包时间、抓包位置、涉及IP。
- 变更凭据。数据库密码、应用Redis密码、服务器SSH密钥全部更换。防止攻击者已经通过Webshell拿到了数据库连接串,在入口被封后仍然能用其他方式连进来。
- 影响范围排查。通过流量和设备告警,检查源IP是否还访问过其他服务器,检查同一网段里是否有同类外联。
- 清理恶意文件并修补漏洞。
- 恢复业务并持续观察。
这个顺序里我特别想强调的是第1步。很多处置方案会建议先登录服务器杀进程、删文件,但如果攻击者保持控制通道,他可能在你清理的同时上传新的Webshell,甚至执行更激进的操作。断网是成本最低、效果最快的止血动作,一定要放在最前面。
4.2 去除Webshell并修补入口
断网取证后,我开始清理和修复。清理本身不复杂:删除/uploads/avatar_20240812_142313.jpg.php这个文件,同时对整个Web目录做一次全盘扫描,按“最近24小时内新建/修改的PHP文件”和“内容里存在高危函数调用”两个维度交叉筛查,确认没有其他变种。
但清理只是治标,真正要治本的是把上传漏洞补上。这个项目的问题是上传接口直接接受了.jpg.php这样的双重后缀文件,而且上传目录开启了脚本执行权限。修复动作分了四层:
- 上传文件名校验:改成白名单后缀模式,只允许jpg、png、gif、webp等图片扩展名;文件名统一由后端生成随机字符串,不再信任用户传入的文件名。
- 上传目录执行权限收敛:在Nginx虚拟主机配置里对
/uploads目录强制不解析PHP,甚至直接设置location ~ \.php$ { deny all; },即使再有PHP文件被传上去,也无法作为脚本执行。 - Content-Type和文件头校验:后端除了检查后缀,还要校验文件的真实内容,比如图片文件需要验证
getimagesize()能正常识别,避免“图片马”绕过。 - 高危函数禁用:在
php.ini的disable_functions里加上assert, system, exec, shell_exec, proc_open, popen, passthru, eval等函数,从语言层面降低Webshell被利用后的破坏力。
这一整套修复做完,我还在同一台服务器上用专门检测Webshell的工具做了一次扫描。注意,扫描工具最好在内网隔离环境或先把规则库更新到最新再离线使用,避免把外联更新也当成可疑行为,干扰判断。
4.3 把分析结论变成可持续的检测规则
清理完不是结束,不把这次拿到的分析结果转成检测规则,下一次还可能倒在同一个坑里。我在WAF和日志告警层面各加了几条规则,思路完全来源于前面流量分析里那三个特征。
WAF侧的检测逻辑可以这么写:
- 如果请求URI匹配上传目录,并且文件后缀同时包含图片后缀和
.php,直接阻断并告警。 - 如果请求URI为
.php结尾,且请求方法为POST,且请求体中单一参数值经过Base64解码后包含高危函数名,则阻断。 - 如果请求头缺少常见浏览器字段,比如缺少
Accept-Language、Accept-Encoding,同时请求体是纯Base64字符集,也纳入可疑会话。
IDS/NDR侧的检测思路则可以组合流量特征:
- POST请求 + UA异常 +
http.file_data只包含Base64字符 + 相应响应长度与请求体长度近似。 - 同源IP在短时间内访问上传接口且文件名带双后缀。
- 固定周期(比如30分钟间隔)访问同一URI且每次都有请求体和响应体。
日志层面的监控我做得更简单粗暴:对文件上传接口记录完整的原始请求包;对Web目录下新增的.php文件生成审计日志;对访问上传目录下PHP文件的请求单独告警。这些规则可能有一些误报,但安全运营宁可先有报警再人工研判,也比什么都没有强。
5. 这次事件里值得改进的监控盲点
5.1 为什么安全设备没拦下来
既然安全设备最终是通过“异常外联”这个行为告警的,那为什么不早点拦住Webshell本身?这个问题我复盘了很久,有几个原因值得写下来。
第一,WAF是规则匹配模式,对已知Webshell特征有效,但攻击者做了混淆。参数名是自定义的,命令payload经过了Base64,WAF如果不对请求体做深度解码,或者只匹配eval、shell_exec等明文关键词,就会直接漏掉。
第二,攻击者控制频率极低。30分钟才回连一次,每次就一两个HTTP请求,这种低频慢速通道对所有基于“频率阈值”的检测模型都是盲区,因为它的统计特征跟正常用户浏览网站几乎没区别。
第三,出站流量管控缺失。当时只对入站做了Web防护和漏洞扫描,没有对出站外联做“默认拒绝”的白名单策略。如果当时出口防火墙只允许这台服务器访问固定的数据库IP和运维跳板机,这个外联会在第一次发生时就被阻断,根本不用等流量的统计分析。
第四,应用层变更审计没有打通。文件上传成功但没有触发文件监控,导致我在流量分析已经确认Webshell时才第一次知道这个文件的存在。如果有一层文件完整性监控,上传文件落地时就应该产生告警。
5.2 我后来长期坚持的几个检查习惯
这次事件之后,我把一些措施沉淀成了固定习惯,这些不一定需要很高成本,但长期坚持下来确实有效:
- 关键Web目录的文件完整性监控。每次版本发布后对Web目录算一遍哈希入库,发布之外的时间如果出现新增或变动,立刻告警。
- 上传目录强制不可执行。在新项目里直接固化到初始化脚本里,不是靠人工记得配,而是环境创建时就默认带上。
- 出站流量白名单制度。核心业务服务器只放行到明确需要的目标地址,其他出站一律拒绝,这个策略的收益远比想象中大。
- 定期Webshell专项扫描。不只扫一次,而是每季度跑一轮,重点扫上传目录和遗留废弃接口。
- 模拟演练。每半年做一次“如果现在出现Webshell告警,半小时内能不能回答:谁上传的、何时上传的、上传到了哪里、执行了什么命令、拿走了什么数据”的演练。这个演练不是为了考核人,而是倒逼流程和工具链保持可用。
再分享一个实际操作中的小技巧:做Webshell流量分析时,pcap文件里很容易混入大量正常业务流量,不要一上来就用Wireshark的图形界面慢慢翻。先用tshark按HTTP方法、URI、响应长度做一个粗筛,锁定可疑请求,再回到Wireshark里Follow TCP Stream看细节,效率会高非常多。像这次如果直接打开20MB的pcap找可疑请求,光是肉眼翻包就够翻半小时,而用命令行过滤只需要几秒钟。安全分析里,先把检索范围缩小到可审阅的粒度,再叠加上下文做判断,永远是最高效的路子。
