内存马是什么
简单说就是没有文件落地的webshell。
常规的java webshell都是上传一个jsp文件,访问这个jsp文件的路径执行命令。但是这样做查文件很容易查出来,文件一删webshell就没了。
内存马是在内存里的,可以访问到但找不到对应路径的文件,后缀也不一定是jsp。更隐蔽,更难排查。
Filter型内存马
内存马有很多种实现方式,filter型内存马是通过filter实现的内存马。
filter是在请求被servlet处理之前执行的一段程序,可以对请求进行拦截、修改。通常用来实现一些业务逻辑之外的功能,比如日志记录、全站加密、权限校验等。
通过修改存储filter的变量,把webshell放在filter里,就是filter型内存马。
环境搭建
装个tomcat。
IDEA里新建项目,Jakarta EE,Web应用程序
应用程序服务器新建,选择tomcat目录,apache-tomcat-10.1.40
点上边运行,如果自动打开了tomcat服务的web,那就成功了。
端口占用的要么把burp关掉,要么改成别的端口,我改成888了。
准备环境
先写个正常的filter
src/main/java/org/example/demo2/filter1.java
1 | package org.example.demo2; |
然后加到web.xml里
src/main/webapp/WEB-INF/web.xml
1 |
|
在pom.xml里加上依赖,不然调试的时候看不到源码:
1 | <dependency> |
到设置里把“不要进入类”取消,不然调试的时候自动跳过一堆
在自己写的filter里的chain.doFilter(request, response);
打断点,调试运行,访问http://localhost:888/demo2_war_exploded/filter1 的时候会断到这里。
Filter调用过程分析
简单来说,运行的时候按照在web.xml里<filter-mapping>
注册的顺序生成一个FilterChain,filter调用的过程就是按顺序执行FilterChain里的filter,每个filter里的chain.doFilter都是调用下一个filter的doFilter,直到走到tomcat自带的最后一个filter调用servlet的service方法。
FilterChain是根据StandardContext里的filterConfigs 、filterDefs 和filterMaps生成的。修改这三个变量就能添加一个可以执行webshell的filter,这就是filter型内存马的注入过程。
详细调试过程参考Java内存马系列-03-Tomcat 之 Filter 型内存马 | Drunkbaby’s Blog
可能是反编译的原因,这篇文章里的代码和实际下载的源码不太一样,尤其是if嵌套的结构上,应该是编译的时候优化了。
这部分不重要,知道是按顺序执行FilterChain里的filter就行
按照上面的操作断到chain.doFilter
之后,跟进去。
1 | public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { |
Globals.IS_SECURITY_ENABLED是False,这个东西是个什么安全管理器的标志,已经弃用了,不用考虑是true的情况。而且就算是true里面也会调用internalDoFilter
,不影响:
到else里的internalDoFilter
,跟进去:
1 | private void internalDoFilter(ServletRequest request, ServletResponse response) |
pos是现在执行的filter的下标(从0开始),n是filter总数,tomcat有一个内置的filter最后执行,加上自定义的是俩。
下边filters[pos++]
取出下一个filter,这个就是tomcat内置的filter,经过一堆乱七八糟的判断和修改之后又调用了这个filter的doFilter,再进去。
1 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) |
这里面调用了FilterChain的doFilter,再进去,发现又到了ApplicationFilterChain的doFilter
里,就是最开始进到的那个:
1 | public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { |
然后到internalDoFilter
里:
1 | private void internalDoFilter(ServletRequest request, ServletResponse response) |
此时pos是2,执行else里的内容,往下走,经过一堆判断之后执行servlet.service
,后面的是servlet里的代码,和filter无关了。
从上面的过程中可以看出filter都存在FilterChain里,filter的调用过程就是遍历FilterChain一个一个的执行。只要能在FilterChain里加上带webshell的filter就可以执行webshell。
ApplicationFilterChain确实有一个addFilter方法。能不能调用addFilter方法添加filter?不能,因为jsp运行的时候拿不到FilterChain,没办法调用它的方法。
FilterChain生成过程分析
在addFilter上打个断点,看看addFilter是在哪调用的。
在ApplicationFilterFactory的createFilterChain
里
1 | public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) { |
可以看到参数有一个filterConfig,找一下filterConfig是哪来的
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
是context的findFilterConfig返回的,进去发现是从context的名为filterConfigs的Map里直接取出来的,key就是参数里传的filterMap.getFilterName()。
再看下filterMap是哪来的
filterMap是遍历filterMaps的元素,filterMaps是context.findFilterMaps();
,进去发现也是从context的Map里取的,Map名是filterMaps。
context是
StandardContext
,StandardContext
代表了一个 Web 应用程序。每个部署到 Tomcat 的 Web 应用都被表示为StandardContext
的一个实例。这个类负责管理与该 Web 应用相关的所有资源和服务,包括但不限于:Servlets、Listeners、Filters、静态资源。
所以只要在context里的filterMaps和filterConfigs里加上自己filter的filterConfig和filterMap就行。
构造filterConfig
filterConfigs就是一个Map,添加元素直接put就行。
filterConfigs里的元素filterConfig是ApplicationFilterConfig,构造方法如下:
1 | ApplicationFilterConfig(Context context, FilterDef filterDef) |
构造方法有两个参数,其中context就是前面的StandardContext,可以打个断点看一下。filterDef是org.apache.tomcat.util.descriptor.web.FilterDef,这个倒是public的不用反射了,就是没有构造方法,要调里边的set方法。打个断点看一下可以发现在**org.apache.catalina.core.ApplicationContext#addFilter(java.lang.String, java.lang.String, jakarta.servlet.Filter)**里有:
1 | private FilterRegistration.Dynamic addFilter(String filterName, String filterClass, Filter filter) |
new FilterDef之后调用了setFilterName
、setFilterClass
和setFilter
。所以构造只set这三个就行。
构造filterMap
filterMaps是一个静态内部类ContextFilterMaps,有一个add
方法和一个addBefore
方法,都能添加filterMap,区别就是一个加后边一个加前边,一般情况下影响不大,除非有filter写的waf,那就只能用addBefore。
filterMap在org.apache.catalina.core.ApplicationFilterRegistration#addMappingForUrlPatterns里用过:
1 | public void addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, |
可以看到new出来以后调了setFilterName
、setDispatcher
、addURLPattern
。自己写的时候也调这些就行。
到这里就差不多了,尝试构造一下
内存马的构造
1 | <%@ page import="java.lang.reflect.Field" %> |
开起来服务访问这个jsp就注入内存马了,这时候访问任意路径带上cmd参数都能 执行命令。
把filterMap添加到filterMaps里的时候直接stc.addFilterMapBefore(filterMap)
就行,里面调了addBefore,不用自己反射调用了:
1 | public void addFilterMapBefore(FilterMap filterMap) { |
明白了原理就可以自己改内存马了,比如从请求头里获取命令,从响应头里获取结果:
1 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { |
扩展
在分析FilterChain的构造过程的时候,发现下面还有一段:
ApplicationFilterFactory.java
1 | // Add filters that match on servlet name second |
<filter-mapping>
其实有两种,一种是上面用的根据url-pattern触发的,另一种是根据servlet-name触发的。ApplicationFilterFactory里的这第二段就是根据servlet-name触发的。
所以构造内存马的时候也可以构造用servlet-name的filterMap
改一下filterMap构造的部分就行:
1 | //构造filterMap |