零基础从0到1掌握Java内存马(2)
解*啊 发表于 江苏 WEB安全 2518浏览 · 2024-02-19 16:51

继续开始我们的第二部分~

文章完整版已开源至Github,觉得写的不错的话就给个star吧~:

https://github.com/W01fh4cker/LearnJavaMemshellFromZero

2.13 Tomcat Valve介绍与运行过程分析

2.13.1 Valve与Pipeline

在众多文章里面,下面的这篇我觉得是讲的最通俗易懂的,这里推荐给大家:

https://www.cnblogs.com/coldridgeValley/p/5816414.html

这里我组合引用原文,做了适当的修改,概括一下:

tomcat中的Container有4种,分别是EngineHostContextWrapper,这4Container的实现类分别是StandardEngineStandardHostStandardContextStandardWrapper4种容器的关系是包含关系,Engine包含HostHost包含ContextContext包含WrapperWrapper则代表最基础的一个Servlet
tomcatConnectorContainer两部分组成,而当网络请求过来的时候Connector先将请求包装为Request,然后将Request交由Container进行处理,最终返回给请求方。而Container处理的第一层就是Engine容器,但是在tomcatEngine容器不会直接调用Host容器去处理请求,那么请求是怎么在4个容器中流转的,4个容器之间是怎么依次调用的呢?

原来,当请求到达Engine容器的时候,Engine并非是直接调用对应的Host去处理相关的请求,而是调用了自己的一个组件去处理,这个组件就叫做pipeline组件,跟pipeline相关的还有个也是容器内部的组件,叫做valve组件。

Pipeline的作用就如其中文意思一样——管道,可以把不同容器想象成一个独立的个体,那么pipeline就可以理解为不同容器之间的管道,道路,桥梁。那Valve这个组件是什么东西呢?Valve也可以直接按照字面意思去理解为阀门。我们知道,在生活中可以看到每个管道上面都有阀门,PipelineValve关系也是一样的。Valve代表管道上的阀门,可以控制管道的流向,当然每个管道上可以有多个阀门。如果把Pipeline比作公路的话,那么Valve可以理解为公路上的收费站,车代表Pipeline中的内容,那么每个收费站都会对其中的内容做一些处理(收费,查证件等)。

Catalina中,4种容器都有自己的Pipeline组件,每个Pipeline组件上至少会设定一个Valve,这个Valve我们称之为BaseValve,也就是基础阀。基础阀的作用是连接当前容器的下一个容器(通常是自己的自容器),可以说基础阀是两个容器之间的桥梁。

Pipeline定义对应的接口Pipeline,标准实现了StandardPipelineValve定义对应的接口Valve,抽象实现类ValveBase4个容器对应基础阀门分别是StandardEngineValveStandardHostValveStandardContextValveStandardWrapperValve。在实际运行中,PipelineValve运行机制如下图:

这张图是新加坡的Dennis JacobApacheCON Asia 2022上的演讲《Extending Valves in Tomcat》中的PPT中的图片,pdf链接如下:

https://people.apache.org/~huxing/acasia2022/Dennis-Jacob-Extending-Valves-in-Tomcat.pdf

这篇演讲的录屏在Youtube上面可以找到:

https://www.youtube.com/watch?v=Jmw-d0kyZ_4

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架构图:

可以清楚地看到,在此之前还有ExecutorProcessor两个模块,本节内容主要讨论后者,在下节中我们会讨论前者。

这一部分需要更加完备的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方法根据upgradedNamehttpUpgradeProtocols拿到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 Upgradedemo,在如下位置打下断点,然后执行命令curl -H "Connection: Upgrade" -H "Upgrade: hello" http://localhost:8080/evil进入断点调试::

step over一步即可在下方看到request1属性:

然后在request1里面的connectorprotocolHandler里面发现了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

https://cjlusec.ldxk.edu.cn/2023/02/15/Executor/

https://xz.aliyun.com/t/11593

https://xz.aliyun.com/t/11613

在我之前提到过的讲tomcat架构的基础文章(https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09),有详细地讲述ProtocolHandler组件中的EndPoint部件,如果之前没有看完整地可以再去看下。里面这张图画的很好,我这里作引用:

2.15.2.1 Endpoint五大组件

如下表所示:

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

2.15.2.2 Endpoint分类

EndPoint接口的具体实现类为AbstractEndpointAbstractEndpoint具体的实现类有AprEndpointNio2EndpointNioEndpoint

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,往下翻即可看到有setExecutorgetExecutor这两个函数:

查看getExecutor函数的调用位置,发现就在该文件中有一个关键调用:

跟过去:

从下面这篇文章中我们可以知道processSocketTomcat运行过程中的作用:

https://blog.51cto.com/u_8958931/2817418

那此时我们就有一个想法,如果我能控制executor,我把原来的executor通过setExecutor变成我恶意创建的executor,然后再通过这后面的executor.executeorg.apache.tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable))一执行就可以加载我们的恶意逻辑了。

但是现在有一个很头疼的问题,那就是标准的ServletRequest需要经过Adapter的封装后才可获得,这里还在Endpoint阶段,其后面封装的ServletRequestServletResponse无法直接获取。

那怎么办呢?结合之前学过的知识,我们很容易想到在之前我们第一次接触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的一部分传出即可。

0 条评论
某人
表情
可输入 255