Tomcat 内存马(一):Filter型
0x00 前言
内存马又名无文件马,见名知意,也就是无文件落地的 webshell 技术,是由于 webshell 特征识别、防篡改、目录监控等等针对 web 应用目录或服务器文件防御手段的介入,导致的文件 shell 难以写入和持久而衍生出的一种“概念型”木马。这种技术的核心思想非常简单,一句话就能概括,那就是对访问路径映射及相关处理代码的动态注册。
这种动态注册技术来源非常久远,在安全行业里也一直是不温不火的状态,直到冰蝎的更新将 java agent 类型的内存马重新带入大众视野并且瞬间火爆起来。这种技术的爆红除了概念新颖外,也确实符合时代发展潮流,现在针对 webshell 的查杀和识别已经花样百出,大厂研发的使用分类、概率等等方式训练的机器学习算法模型,基于神经网络的流量层面的特征识别手段,基本上都花式吊打常规文件型 webshell。如果你不会写,不会绕,还仅仅使用网上下载的 jsp ,那肯定是不行的。
内存马搭上了冰蝎和反序列化漏洞的快车,快速占领了人们的视野,成为了主流的 webshell 写入方式。作为 RASP 技术的使用者,自然也要来研究和学习一下内存马的思想、原理、添加方式,并探究较好、较通用的防御和查杀方式。
目前安全行业主要讨论的内存马主要分为以下几种方式:
- 动态注册servlet/filter/listener(使用servlet-api的具体实现)
- 动态注册interceptor/controller(使用框架如spring/strusts2)
- 动态注册使用职责链设计模式的中间件、框架的实现(例如Tomcat的Pipeline & Value,Grizzly的FilterChain &FIlter等等)
- 使用java agent技术写入字节码
0x01 Tomcat 简介
这里简单的介绍一下,具体的可以看下方参考链接内容
参考链接:https://www.cnblogs.com/whgk/p/6399262.html
Servlet
Servlet是一种处理请求和发送响应的程序,Servlet是为了解决动态网页而衍生的技术
Tomcat与Servlet的关系
Tomcat是Web应用服务器,是一个Servlet/JSP的容器,Tomcat作为Servlet的容器,能够将用户的请求发送给Servlet,并且将Servlet的响应返回给用户。
Tomcat中有四种类型的Servlet容器,从上到下分别是Engine、Host、Context、Wrapper
- Engine,实现类为
org.apache.cataline.core.StrandardEngine
- Host,实现类为
org.apache.catalina.core.StandardHost
- Context,实现类为
org.apache.catalina.core.StandardContext
- Wrapper,实现类为
org.apache.catalina.core.StandardWrapper
每个Wrapper实例表示一个具体的Servlet定义,StrandardWrapper是Wrapper接口的标准实现类(StrandardWrapper的主要任务就是载入Servlet类并且进行实例化)
Tomcat 容器
在Tomcat中,每个Host下可以有多个Context(Context是Host的子容器),每个Context都代表一个具体的web应用,都有一个唯一的路径就是相当于下图中的 /shop
/manage
这种,在一个Context下可以有着多个Wrapper
Wrapper主要主要负责管理Servlet的装载、初始化、执行以及资源回收
0x02 内存马简单介绍
由于现在各种防护措施越来越多,文件shell就如c0ny1师傅所说的大部分已经气数已尽,内存马因其隐蔽性等优点从而越来越盛行
内存马主要分为以下几类:
- servlet-api类
- filter型
- servlet型
- spring类
- 拦截器
- controller型
- java Instrumentation类
- agent型
0x03 Tomcat Filter 流程
Filter被我们称之为过滤器,是Java中最常见也是最实用的技术之一,通常被用来处理静态web资源和访问权限的控制和过滤、记录日志等附加功能等等。一次请求进入到服务器后,会先由Filter进行处理,再交给Servlet。
通常Filter配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:
- 使用
ServletContext
的addFilter/createFilter
方法注册。 - 使用
ServletContextListener
的contextInitialized
方法在服务器启动时注册。 - 使用
ServletContainerInitializer
的onStartup
方法在初始化时注册(非动态)
本节只讨论使用 ServletContext
添加 Filter 内存马的方法。首先来看一下 createFilter
方法,按照注释,这个类用来在调用 addFilter
向 ServletContext
实例化一个指定的 Filter 类。
在具体分析流程之前我们先介绍一下后面会遇到的几个类:
- FilterDefs:存放FilterDef的数组,FilterDef中存放我们顾虑器名,过滤器实例,作用范围(url)等基本信息
- FilterConfigs:存放FIlterConfig的数组,在FIlterConfig中主要存放FilterDef和Fitler对象信息
- FilterMaps:存放FilterMap的数组,在FilterMap中主要存放了FilterName和对应的URLPattern
- FilterChain:过滤器链,该对象上的doFilter方法能一次调用链上的Filter
- WebXml:存放web.xml中内容的类
- ContextConfig:web应用的上下文配置类
- StandardContext:Context接口的标准实现类,一个Context代表一个Web应用,其下可以包含多个Wrapper
- StandardWrapperValue:一个Wrapper的标准实现类,一个Wrapper代表一个Servlet
接下来我们来分析一下 Tomcat 中是如何将我们自定义的 filter 进行设置并且调用的
首先会通过 configureContext 解析 web.xml 然后返回 webXml 实例
在 StandardWrapperValve 中会利用 ApplicationFilterFactory 来创建 filterChain(filter链),我们跟进这个方法
我们看到红框处的代码,首先会调用 getParent 获取当前 Context (即当前 Web应用),然后会从 Context 中获取到 filterMaps
filterMaps中的 filterMap 主要存放了过滤器的名字以及作用的 url,继续往下看
发现会遍历 FilterMaps 中的 FilterMap,如果发现符合当前请求 url 与 FilterMap 中的 urlPattern 相匹配,就会进入 if 判断会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入 if 判断,将 filterConfig 添加到 filterChain中,跟进addFilter函数
在addFilter函数中首先会遍历filters,判断我们的filter是否已经存在(其实就是去重)
下面这个 if 判断其实就是扩容,如果 n 已经等于当前 filters 的长度了就再添加10个容量,最后将我们的filterConfig 添加到 filters中
至此 filterChain 组装完毕,重新回到 StandardContextValue 中,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上的 doFilter方法
在 doFilter 方法中会调用 internalDoFilter方法
在internalDoFilter方法中首先会依次从 filters 中取出 filterConfig
然后会调用 getFilter() 将 filter 从 filterConfig 中取出,调用 filter 的 doFilter方法
从而调用我们自定义过滤器中的 doFilter 方法,从而触发了相应的代码
最后放一张来自宽字节安全的图
总结:
- 根据请求的 URL 从 FilterMaps 中找出与之 URL 对应的 Filter 名称
- 根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
- 找到对应的 FilterConfig 之后添加到 FilterChain 中,并且返回 FilterChain
- 根据 FilterChain 调用 internalDoFilter 方法遍历获取 chain 中的 FilterConfig,然后从 FilterConfig 中获取 Filter,调用 Filter 的doFilter方法
根据上面的简单总结,我们可以发现最开始是从 context 中获取的 FilterMaps, 然后将符合条件的依次按照顺序进行调用。
那么我们可以自己创建一个 FilterMap 然后将其放在 FilterMaps 的最前面,这样当 urlpattern 匹配到的时候就会找到对应的 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发我们的内存 shell
0x04 Filter 型内存马
该方法只能在 tomcat 7.x 以上利用具体原因会在后文提到
前面说到当组装我们的过滤器链的时候,是从 context 中获取到的 FilterMaps
那么我们如何获取这个 context 呢?
当我们能直接获取 request 的时候,我们可以直接使用如下方法
将我们的 ServletContext 转为 StandardContext 从而获取 context
ps:当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用
1 | ServletContext servletContext = request.getSession().getServletContext(); |
其他的获取 context 的方法( 暂时还未仔细研究,所以这里就先放链接了)
从线程中获取StandardContext
如果没有request对象的话可以从当前线程中获取
从MBean中获取
获取到 Context 之后 ,我们可以发现其中的 filterConfigs,filterDefs,filterMaps 这三个参数和我们的 filter 有关,那么如果我们可以控制这几个变量那么我们或许就可以注入我们的内存马
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
filterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
大致流程如下:
- 创建一个恶意的 Filter
- 利用 FilterDef 对 Filter 进行一个封装
- 将 FilterDef 添加到 FilterDefs 和 FilterConfig
- 创建 FilterMap,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps 中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
每次请求 creatFilterChain 都会依据此动态生成一个过滤链,而 StandardContext 又会一直保留在 Tomcat 生命周期结束,所以我们的内存马就可以一直驻留下去,直到 Tomcat 重启
1 | Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); |
ps:前文说到该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
1 | filterMap.setDispatcher(DispatcherType.REQUEST.name()); |
最终内存马如下:
1 | <%@ page import="org.apache.catalina.core.ApplicationContext" %> |
开启服务,访问 filterdemo02.jsp 显示注入成功
然后只需 ?cmd=Command
即可执行我们的命令
0x05 排查内存马的几个方法
参考链接:https://syst1m.com/post/memory-webshell/#arthas
arthas
项目链接:https://github.com/alibaba/arthas
我们可以利用该项目来检测我们的内存马
下载arthas-boot.jar
,然后用java -jar
的方式启动:
1 | curl -O https://arthas.aliyun.com/arthas-boot.jar |
这里选择我们 Tomcat 的进程
通过mbean
命令,可以便捷的查看或监控 Mbean 的属性信息(可以查看异常Filter
/Servlet
节点)
- mbean | grep Filter
- mbean | grep Servlet
利用 sc *.Filter
进行模糊搜索,会列出所有调用了 Filter 的类?
利用jad --source-only org.apache.jsp.filterdemo02_jsp
直接将 Class 进行反编译
同时也可以进行监控 ,当我们访问 url 就会输出监控结果
java-memshell-scanner
项目链接:https://github.com/c0ny1/java-memshell-scanner
c0ny1 师傅写的检测内存马的工具,能够检测并且进行删除,是一个非常方便的工具
该工具是由 jsp 实现的,我们这里主要来学习一下 c0ny1 师傅 删除内存马的逻辑
检测是通过遍历 filterMaps 中的所有 filterMap 然后显示出来,让我们自己认为判断,所以这里提供了 dumpclass
删除的话,这里主要是通过反射调用 StandardContext#removeFilterDef 方法来进行删除
0x06 总结
filter类型的内存马只是内存马的一种,本文中内存马还是需要上传 jsp 来生效,但是实际上利用方式远不止这样,我们还可以借助各种反序列化来动态注册 Filter 等。
- 本文作者: y0lo
- 本文链接: http://example.com/2022/03/08/java内存马01/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!