零基础从0到1掌握Java内存马(2)
继续开始我们的第二部分~ 文章完整版已开源至Github,觉得写的不错的话就给个star吧~: https://github.com/W01fh4cker/LearnJavaMemshellFromZero 2.13 Tomcat Valve介绍与运行过程分析 2.13.1 Valve与Pipeline 在众多文章里面,下面的这篇我觉得是讲的最通俗易懂的,这里推荐给大家: https://www
继续开始我们的第二部分~
文章完整版已开源至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的一部分传出即可。

