记一次企业门户网站被植入 Webshell 的入侵溯源与清除

对外门户 Tomcat 疑似加密外联,顺藤摸到文件上传接口被绕过植入哥斯拉 Webshell 的溯源清剿全过程

问题背景

我们对外的企业门户网站是一套基于 Tomcat 9 + Spring MVC 的老系统,跑在一台放在 DMZ 区的 Linux 服务器上,前面是 Nginx 反向代理,再往外是防火墙做端口映射。这套系统上线好几年,功能改动不多,一直是"能跑就不动"的状态。

某天下午,全流量检测设备(NDR)弹出一条告警:这台 Web 服务器在持续向一个境外 IP 发起可疑加密 HTTP 通信,特征命中"疑似哥斯拉(Godzilla)Webshell 加密流量"。安全同事把工单转给我时,第一反应是"会不会是误报"——毕竟这台机器只是个门户站,没什么敏感数据。但告警的置信度不低,且外联是持续性的,只能按真出事来应急处理。

故障现象

登上服务器先做了一圈粗看,几个现象拼在一起就不像误报了:

  • CPU 间歇性飙高top 里 Tomcat 的 java 进程偶尔冲到 80% 以上,但门户站访问量并不大,业务上没有理由这么忙。
  • 异常外联确实存在netstat -antp 能看到该 java 进程与告警里那个境外 IP 建立着 ESTABLISHED 连接,端口是 443,流量是加密的,抓包也看不出明文。
  • 网站被挂了暗链:用搜索引擎 site: 检索自己的域名,翻到几个根本不存在的博彩、菠菜类页面路径,点进去会 302 跳转到外部站——典型的被挂马 / SEO 暗链。
  • 上传目录里多了陌生文件:门户有个"个人头像上传"功能,落地目录 /data/webapp/portal/upload/avatar/ 下,除了正常的 jpg/png,还躺着几个 .jsp 文件,修改时间集中在三天前的凌晨。

到这一步基本可以确认:不是误报,是真被人打进来了,且已经落地了 Webshell、并在拿这台机器做外联跳板和挂暗链。

排查过程

第一步,先确认外联的"主体"是谁。netstat -antp | grep <境外IP> 拿到连接对应的 PID,再 ps -ef | grep <PID>,结果指向的是 Tomcat 自己的 java 进程,而不是某个独立的挖矿木马或反弹 shell 进程。这说明恶意行为是跑在 Web 容器进程里的——要么是落地的脚本 Webshell 被调用,要么是更隐蔽的内存马。

第二步,翻 Web 访问日志找入口。 重点看 Nginx 的 access.log 和 Tomcat 的 localhost_access_log。Webshell 的访问有很强的特征,用几个条件一过滤就浮出来了:

1
2
3
4
5
# 找对单个 URL 高频 POST 的可疑请求
awk '$6=="POST"{print $7}' access.log | sort | uniq -c | sort -rn | head

# 结合异常 UA / 固定 Content-Length 规律进一步收敛
grep "avatar/.*\.jsp" access.log | awk '{print $1,$4,$7,$9,$10}'

结果非常清楚:/upload/avatar/x1.jsp 这个路径在过去三天里被同一批源 IP 反复 POST,请求体长度呈规律性变化,User-Agent 是 Java 客户端的默认值——这正是哥斯拉客户端连接 Webshell 的典型行为。

第三步,确认落地文件性质。 打开那几个 .jsp,内容不是明文,而是一段加载器:接收 pass 参数,做 Base64 解码后再 AES 解密,动态编译执行——标准的哥斯拉加密马,磁盘上和流量里都是密文,普通特征匹配很难抓。

第四步,回溯 Webshell 是怎么传上来的。 门户的头像上传接口按理只允许图片。翻代码 + 翻上传接口的请求日志,还原出攻击者的绕过手法:

  • 服务端对文件类型的校验用的是黑名单(只拦 .php.asp),没拦 .jsp
  • 且校验只看了前端传来的文件后缀字符串,没有做服务端 MIME / 文件头(magic number)校验;
  • 最致命的是,上传目录就在 Web 根目录下,且被 Tomcat 当作可执行的 JSP 目录——文件传上去就能直接被当脚本解析执行。

三个问题叠加,就是一条完整的"任意文件上传 → Webshell 落地 → 直接访问执行"的链路。

第五步,排查有没有内存马。 落地文件好清,但更怕对方在容器里注册了反射型的 Filter / Servlet 内存马(磁盘无 class、重启才消失)。对比了 Tomcat 当前已加载的 Filter 链与应用 web.xml 声明的差异,又用 arthas 扫了一遍可疑的动态注册组件,确认这次没有内存马,攻击者只用了落地脚本马。

解决方案

确认范围后按应急响应流程处置,顺序很重要:

  1. 先隔离、后动手:在防火墙上只放行运维跳板的入站、掐掉那条境外外联,但暂不整机断网、不重启,先保留现场做取证。
  2. 固定证据:打包 access.log、Webshell 文件、/tmp 与 upload 目录、进程与网络快照,计算哈希留存。
  3. 清除落地马:删除 upload 目录下全部 .jsp 文件,全盘 find / -name "*.jsp" -newermt "3 days ago" 复核有没有藏在别处的第二落点。
  4. 清内存并重启:处理完落地文件后重启 Tomcat,确保即便有残留动态组件也被清掉。
  5. 修上传接口(根治):改为扩展名白名单 + 服务端 magic number 校验 + 随机重命名,并把上传目录移出 Web 根、改为独立存储 + 禁止脚本执行
  6. 收尾加固:轮转服务器口令与相关密钥、清理被挂的暗链页面、通知搜索引擎重新收录。

根因分析

技术根因是三点缺陷的叠加,缺一不可:文件类型校验用黑名单且不全(漏了 .jsp)→ 只信前端后缀、无服务端内容校验 → 上传目录与 Web 根同源且可执行脚本。任何一环补上,这条攻击链都走不通。而管理上的根因是这套"能跑就不动"的老系统长期无人做安全基线审视,暴露面(对外的可上传接口)一直无监控,直到全流量设备兜底才发现。

预防措施

  • 上传接口:扩展名白名单 + 服务端 MIME / 文件头双重校验 + 上传后随机重命名,从源头堵住可控文件名。
  • 存储与执行分离:上传目录独立存放,Nginx / Tomcat 层面显式禁止该目录解析 jsp/php(如 location 配置移除脚本 handler),即使传上去也无法执行。
  • 最小权限运行:Web 容器用非 root 的低权限用户跑,限制其对系统目录的写权限。
  • 纵深检测:部署 RASP / Webshell 检测,定期用河马、D 盾等工具做落地文件基线扫描;保留全流量 NDR 兜底加密马。
  • 暴露面治理:对外系统定期做资产梳理与漏洞扫描,把"能跑就不动"的老站纳入常态化安全巡检。

总结

这次事件的隐蔽点在于:外联主体是 Tomcat 自己、木马是加密的、入口是个不起眼的头像上传接口,任何一个单看都容易被当成"业务正常"或"误报"放过。真正把线索串起来的是全流量告警 + Web 访问日志的高频 POST 特征分析这两把钥匙。对运维来说,最该记住的不是"哥斯拉长什么样",而是:任意文件上传永远要按白名单 + 内容校验 + 存储执行分离三件套来防,而不是靠拦几个危险后缀;同时对外暴露面必须有日志、有监控、有兜底,别让老系统成为攻击者最舒服的落脚点。

使用 Hugo 构建
主题 StackJimmy 设计