天天看点

渗透测试-对新型内存马webshell的研究

内存马webshell-MemoryShell

  • ​​前言:你马没了​​
  • ​​利用JavaAgent技术发现并清除系统中的内存马​​
  • ​​介绍​​
  • ​​安全行业主要讨论的内存马主要分为以下几种方式​​
  • ​​写入测试​​
  • ​​Servlet API 提供的动态注册机制​​
  • ​​Filter 内存马​​
  • ​​使用 ServletContext 添加 Filter 内存马的方法。​​
  • ​​Servlet 内存马​​
  • ​​Listener 内存马​​
  • ​​应用中可能调用的监听器如下​​

前言:你马没了

利用JavaAgent技术发现并清除系统中的内存马

Github:https://github.com/su18/MemoryShell

介绍

内存马又名无文件马,见名知意,也就是无文件落地的 webshell 技术,

是由于 webshell 特征识别、防篡改、目录监控等等

针对 web 应用目录或服务器文件防御手段的介入,

导致的文件 shell 难以写入和持久而衍生出的一种“概念型”木马。

这种技术的核心思想非常简单,一句话就能概括,

那就是对访问路径映射及相关处理代码的动态注册。

这种动态注册技术来源非常久远,在安全行业里也一直是不温不火的状态,

直到冰蝎的更新将 java agent 类型的内存马重新带入大众视野

并且瞬间火爆起来。这种技术的爆红除了概念新颖外,

也确实符合时代发展潮流,

现在针对 webshell 的查杀和识别已经花样百出,

大厂研发的使用​​

​分类、概率​

​​等等方式训练的​

​机器学习算法模型​

​​,

基于​​

​神经网络​

​​的​

​流量​

​​层面的​

​特征识别手段​

​​,

基本上都花式吊打常规文件型 webshell。

如果你不会写,不会绕,还仅仅使用网上下载的 jsp ,那肯定是不行的。

内存马搭上了

冰蝎和反序列化漏洞的快车,快速占领了人们的视野,成为了主流的 webshell 写入方式。

作为 RASP 技术的使用者,

自然也要来研究和学习一下内存马的思想、原理、添加方式,

并探究较好、较通用的防御和查杀方式。

安全行业主要讨论的内存马主要分为以下几种方式

• 动态注册 servlet/filter/listener(使用 servlet-api 的具体实现)

• 动态注册 interceptor/controller(使用框架如 spring/struts2)

• 动态注册使用职责链设计模式的中间件、框架的实现(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter 等等)

• 使用 java agent 技术写入字节码

写入测试

Servlet API 提供的动态注册机制

2013 年,国际大站 p2j 就发布了这种特性的一种使用方法:

渗透测试-对新型内存马webshell的研究

​Servlet、Listener、Filter ​

​由 javax.servlet.ServletContext 去加载

无论是使用 xml 配置文件

还是使用 Annotation 注解配置

均由 Web 容器进行初始化,

读取其中的配置属性,然后向容器中进行注册。Servlet 3.0 API 允许使 ServletContext 用动态进行注册

在 Web 容器初始化的时候(即建立ServletContext 对象的时候)进行动态注册。

可以看到 ServletContext 提供了 add*/create* 方法来实现动态注册的功能。

渗透测试-对新型内存马webshell的研究

在不同的容器中,实现有所不同,这里仅以 Tomcat 为例调试,其他中间件在代码中有部分实现。

Filter 内存马

Filter 我们称之为过滤器,

是 Java 中最常见也最实用的技术之一,

通常被用来​​

​处理静态 web 资源​

​​、​

​访问权限控制​

​​、​

​记录日志​

​​等附加功能等等。

一次请求进入到服务器后,将先由 Filter 对用户请求进行​​

​预处理​

​,再交给 Servlet。

通常情况下,Filter 配置在​

​配置文件​

​​和​

​注解​

​中,在其他代码中如果想要完成注册,主要有以下几种方式:

1. 使用 ServletContext 的 addFilter/createFilter 方法注册;

2. 使用 ServletContextListener 的 contextInitialized 方法在服务器启动时注册
(将会在 Listener 中进行描述);

3.      

使用 ServletContext 添加 Filter 内存马的方法。

看一下 createFilter 方法

按照注释,这个类用来在调用 ​​

​addFilter ​

​​向 ServletContext ​

​实例化​

​一个指定的 Filter 类。

渗透测试-对新型内存马webshell的研究

这个类还约定了一个事情,那就是

如果这个 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 中。

渗透测试-对新型内存马webshell的研究

可以看到,这个方法创建了一个 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

渗透测试-对新型内存马webshell的研究

在 ApplicationFilterRegistration 的 addMappingForUrlPatterns 中生成了 filterMaps。

渗透测试-对新型内存马webshell的研究

而这两者的信息都是从 filterDefs 中的对象获取的。

在了解了上述逻辑后,在应用程序中动态的添加一个 filter 的思路就清晰了:

• 调用 ApplicationContext 的 addFilter 方法创建 filterDefs 对象,需要反射修改应用程序的运行状态,加完之后再改回来;

• 调用 StandardContext 的 filterStart 方法生成 filterConfigs;

• 调用 ApplicationFilterRegistration 的 addMappingForUrlPatterns 生成 filterMaps;

• 为了兼容某些特殊情况,将我们加入的 filter 放在 filterMaps 的第一位,
可以自己修改 HashMap 中的顺序,
也可以在自己调用 StandardContext 的 addFilterMapBefore 直接加在 filterMaps 的第一位。      

基于以上思路的实现在 threedr3am 师傅的 文章 中有实现代码,

既然知道了需要修改的关键位置,那就没有必要调用方法去改,直接用反射加进去就好了,

其中中间还有很多小细节可以变化。

写一个 demo 模拟一下动态添加一个 filter 的过程。

首先我们有一个 IndexServlet,如果请求参数有 id 的话,则打印在页面上。

渗透测试-对新型内存马webshell的研究

现在我们想实现在程序运行过程中动态添加一个 filter ,

提供将 id 参数的数字值 + 3 的功能(随便瞎想的功能。)

具体代码放在了 org.su18.memshell.web.servlet.AddTomcatFilterServlet 中

普通访问时,会将 id 的值打印出来

渗透测试-对新型内存马webshell的研究

访问添加 filter。

渗透测试-对新型内存马webshell的研究

再次访问,id 参数会被加三。

渗透测试-对新型内存马webshell的研究

Servlet 内存马

Servlet 是 Server Applet(服务器端小程序)的缩写,用来读取客户端发送的数据,处理并返回结果。也是最常见的 Java 技术之一。

与 Filter 相同,本小节也仅仅讨论使用 ServletContext 的相关方法添加 Servlet。

还是首先来看一下实现类 ApplicationContext 的 addServlet 方法。

渗透测试-对新型内存马webshell的研究

与上一小节看到的 addFilter 方法十分类似。

那么我们面临同样的问题,在一次访问到达 Tomcat 时,是如何匹配到具体的 Servlet 的?

这个过程简单一点,只有两部走:

• ApplicationServletRegistration 的 addMapping 方法调用 StandardContext#addServletMapping 方法,
在 mapper 中添加 URL 路径与 Wrapper 对象的映射(Wrapper 通过 this.children 中根据 name 获取)

• 同时在 servletMappings 中添加 URL 路径与 name 的映射。      

直接调用相关方法进行添加,当然是用反射直接写入也可以,有一些逻辑较为复杂。

测试代码在 org.su18.memshell.web.servlet.AddTomcatServlet 中,

访问这个 servlet 会在程序中生成一个新的 Servlet :/su18。

渗透测试-对新型内存马webshell的研究

看一下效果。

渗透测试-对新型内存马webshell的研究

Listener 内存马

Servlet 和 Filter 是程序员常接触的两个技术,

所以在网络上对于之前两小节的讨论较多,

对于 Listener 的讨论较少。但实际上这个点还是有很多师傅关注到了。

Listener 可以译为监听器,监听器用来监听对象或者流程的创建与销毁,通过 Listener,可以自动触发一些操作,

因此依靠它也可以完成内存马的实现。

先来了解一下 Listener 是干什么的,看一下 Servlet API 中的注释。

渗透测试-对新型内存马webshell的研究

应用中可能调用的监听器如下

.servlet.http.HttpSessionListener:对 Session 整体状态的监听

• javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听      

可以看到 Listener 也是为一次访问的请求或生命周期进行服务的,

在上述每个不同的接口中,都提供了不同的方法,用来在监听的对象发生改变时进行触发。

而这些类接口,实际上都是 java.util.EventListener 的子接口。

这里我们看到,在 ServletRequestListener 接口中,提供了两个方法在 request 请求创建和销毁时进行处理,比较适合我们用来做内存马。

渗透测试-对新型内存马webshell的研究

除了这个 Listener,其他的 Listener 在某些情况下也可以触发作为内存马的实现.

ServletRequestListener 提供两个方法:

requestInitialized 和 requestDestroyed,

两个方法均接收 ServletRequestEvent 作为参数,

ServletRequestEvent 中又储存了 ServletContext 对象和 ServletRequest 对象,

因此在访问请求过程中我们可以在 request 创建和销毁时实现自己的恶意代码,完成内存马的实现。

渗透测试-对新型内存马webshell的研究

Tomcat 中 EventListeners 存放在

StandardContext 的 applicationEventListenersObjects 属性中,

同样可以使用 StandardContext 的相关 add 方法添加。

我们还是实现一个简单的功能,在 requestDestroyed 方法中获取 response 对象,

向页面原本输出多写出一个字符串。正常访问时:

渗透测试-对新型内存马webshell的研究

添加 Listener,可以看到,

由于我们是在 requestDestroyed 中植入恶意逻辑,那么在本次请求中就已经生效了:

渗透测试-对新型内存马webshell的研究

访问之前的路径也生效了:

渗透测试-对新型内存马webshell的研究

除了 EventListener,Tomcat 还存在了一个 LifecycleListener ,

当然也肯定有可以用来触发的实现类,但是用起来一定是不如 ServletRequestListener

由于在 ServletRequestListener 中可以获取到 ServletRequestEvent,

这其中又存了很多东西,ServletContext/StandardContext 都可以获取到,那玩法就变得更多了。

在 requestInitialized 中监听,如果访问到了某个特定的 URL,

或这次请求中包含某些特征(可以拿到 request 对象,随便怎么定义),

则新起一个线程去 StandardContext 中注册一个 Filter,可以实现某些恶意功能。

在 requestDestroyed 中再起一个新线程 sleep 一定时间后将我们添加的 Filter 卸载掉。

有了一个真正的动态后门,只有用的时候才回去注册它,用完就删。